diff --git a/.devcontainer/Prowlarr.code-workspace b/.devcontainer/Prowlarr.code-workspace new file mode 100644 index 000000000..a46158e44 --- /dev/null +++ b/.devcontainer/Prowlarr.code-workspace @@ -0,0 +1,13 @@ +// This file is used to open the backend and frontend in the same workspace, which is necessary as +// the frontend has vscode settings that are distinct from the backend +{ + "folders": [ + { + "path": ".." + }, + { + "path": "../frontend" + } + ], + "settings": {} +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..70473224d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "Prowlarr", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "16", + "nvmVersion": "latest" + } + }, + "forwardPorts": [9696], + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f33a02cd1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/labeler.yml b/.github/labeler.yml index 21aacef8c..74160b634 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,19 +1,31 @@ 'Area: API': - - src/Prowlarr.Api.V1/**/* + - changed-files: + - any-glob-to-any-file: + - src/Prowlarr.Api.V1/**/* 'Area: Db-migration': - - src/NzbDrone.Core/Datastore/Migration/* + - changed-files: + - any-glob-to-any-file: + - src/NzbDrone.Core/Datastore/Migration/* 'Area: Download Clients': - - src/NzbDrone.Core/Download/Clients/**/* + - changed-files: + - any-glob-to-any-file: + - src/NzbDrone.Core/Download/Clients/**/* 'Area: Indexer': - - src/NzbDrone.Core/Indexers/**/* + - changed-files: + - any-glob-to-any-file: + - src/NzbDrone.Core/Indexers/**/* 'Area: Notifications': - - src/NzbDrone.Core/Notifications/**/* + - changed-files: + - any-glob-to-any-file: + - src/NzbDrone.Core/Notifications/**/* 'Area: UI': - - frontend/**/* - - package.json - - yarn.lock + - changed-files: + - any-glob-to-any-file: + - frontend/**/* + - package.json + - yarn.lock \ No newline at end of file diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml index 8f35f6bd6..77c35366c 100644 --- a/.github/workflows/label-actions.yml +++ b/.github/workflows/label-actions.yml @@ -18,6 +18,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/label-actions@v3 + - uses: dessant/label-actions@v4 with: process-only: 'issues, prs' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 857cfb4a7..ab2292824 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index cf38066c5..1d50cb1f1 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '90' diff --git a/.gitignore b/.gitignore index d903078ef..689b44415 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ coverage*.xml coverage*.json setup/Output/ *.~is +.mono # VS outout folders bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7a36fefe1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "ms-dotnettools.csdevkit", + "ms-vscode-remote.remote-containers" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..d13f9426e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": "Run Prowlarr", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build dotnet", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/_output/net6.0/Prowlarr", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..b3e22f6d1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build dotnet", + "command": "dotnet", + "type": "process", + "args": [ + "msbuild", + "-restore", + "${workspaceFolder}/src/Prowlarr.sln", + "-p:GenerateFullPaths=true", + "-p:Configuration=Debug", + "-p:Platform=Posix", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Prowlarr.sln", + "-property:GenerateFullPaths=true", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/Prowlarr.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Logo/dottrace.svg b/Logo/dottrace.svg deleted file mode 100644 index b879517cd..000000000 --- a/Logo/dottrace.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Logo/jetbrains.svg b/Logo/jetbrains.svg deleted file mode 100644 index 75d4d2177..000000000 --- a/Logo/jetbrains.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Logo/resharper.svg b/Logo/resharper.svg deleted file mode 100644 index 24c987a78..000000000 --- a/Logo/resharper.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Logo/rider.svg b/Logo/rider.svg deleted file mode 100644 index 82da35b0b..000000000 --- a/Logo/rider.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - rider - - - - - - - - - - - - - - diff --git a/Logo/webstorm.svg b/Logo/webstorm.svg deleted file mode 100644 index 39ab7eb97..000000000 --- a/Logo/webstorm.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index 4973be54d..e8c60546a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Prowlarr [![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop) -[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget) +[![Translation status](https://translate.servarr.com/widget/servarr/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget) [![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation/docker) ![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg) [![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers) @@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## JetBrains -Thank you to [JetBrains JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. +Thank you to [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. -- [ReSharper ReSharper](http://www.jetbrains.com/resharper/) -- [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) -- [Rider Rider](http://www.jetbrains.com/rider/) -- [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) +* [ReSharper ReSharper](http://www.jetbrains.com/resharper/) +* [WebStorm WebStorm](http://www.jetbrains.com/webstorm/) +* [Rider Rider](http://www.jetbrains.com/rider/) +* [dotTrace dotTrace](http://www.jetbrains.com/dottrace/) ### License - [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Copyright 2010-2022 +- Copyright 2010-2024 Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c7ab632ae..dc667e803 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,18 +9,18 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '1.10.5' + majorVersion: '1.35.0' minorVersion: $[counter('minorVersion', 1)] prowlarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' sentryOrg: 'servarr' sentryUrl: 'https://sentry.servarr.com' - dotnetVersion: '6.0.417' - nodeVersion: '16.X' - innoVersion: '6.2.0' + dotnetVersion: '6.0.427' + nodeVersion: '20.X' + innoVersion: '6.2.2' windowsImage: 'windows-2022' - linuxImage: 'ubuntu-20.04' - macImage: 'macOS-11' + linuxImage: 'ubuntu-22.04' + macImage: 'macOS-13' trigger: branches: @@ -166,10 +166,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: NodeTool@0 + - task: UseNode@1 displayName: Set Node.js version inputs: - versionSpec: $(nodeVersion) + version: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1075,10 +1075,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: NodeTool@0 + - task: UseNode@1 displayName: Set Node.js version inputs: - versionSpec: $(nodeVersion) + version: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1169,12 +1169,12 @@ stages: submodules: true - powershell: Set-Service SCardSvr -StartupType Manual displayName: Enable Windows Test Service - - task: SonarCloudPrepare@1 + - task: SonarCloudPrepare@3 condition: eq(variables['System.PullRequest.IsFork'], 'False') inputs: SonarCloud: 'SonarCloud' organization: 'prowlarr' - scannerMode: 'MSBuild' + scannerMode: 'dotnet' projectKey: 'Prowlarr_Prowlarr' projectName: 'Prowlarr' projectVersion: '$(prowlarrVersion)' @@ -1187,25 +1187,21 @@ stages: ./build.sh --backend -f net6.0 -r win-x64 TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage displayName: Coverage Unit Tests - - task: SonarCloudAnalyze@1 + - task: SonarCloudAnalyze@3 condition: eq(variables['System.PullRequest.IsFork'], 'False') displayName: Publish SonarCloud Results - - task: reportgenerator@4 + - task: reportgenerator@5.3.11 displayName: Generate Coverage Report inputs: reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined' reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' - - task: PublishCodeCoverageResults@1 - displayName: Publish Coverage Report - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: './CoverageResults/combined/Cobertura.xml' - reportDirectory: './CoverageResults/combined/' + publishCodeCoverageResults: true - stage: Report_Out dependsOn: - Analyze + - Installer - Unit_Test - Integration - Automation diff --git a/build.sh b/build.sh index d282db86a..5139dba52 100755 --- a/build.sh +++ b/build.sh @@ -254,7 +254,7 @@ InstallInno() ProgressStart "Installing portable Inno Setup" rm -rf _inno - curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe" + curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe" mkdir _inno ./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno rm innosetup.exe diff --git a/docs.sh b/docs.sh old mode 100644 new mode 100755 index ae11bc83f..38b0e0fbc --- a/docs.sh +++ b/docs.sh @@ -1,13 +1,18 @@ +#!/bin/bash +set -e + +FRAMEWORK="net6.0" PLATFORM=$1 +ARCHITECTURE="${2:-x64}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-x64" + RUNTIME="win-$ARCHITECTURE" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-x64" + RUNTIME="linux-$ARCHITECTURE" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-x64" + RUNTIME="osx-$ARCHITECTURE" else - echo "Platform must be provided as first arguement: Windows, Linux or Mac" + echo "Platform must be provided as first argument: Windows, Linux or Mac" exit 1 fi @@ -21,17 +26,23 @@ slnFile=src/Prowlarr.sln platform=Posix + if [ "$PLATFORM" = "Windows" ]; then + application=Prowlarr.Console.dll +else + application=Prowlarr.dll +fi + dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Release dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet new tool-manifest -dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 7.3.2 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 & +dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & -sleep 30 +sleep 45 kill %1 diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 8d844cb8b..56eaaeaab 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -357,11 +357,16 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - 'no-shadow': 'off', - // These should be enabled after cleaning things up - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'after-used', + argsIgnorePattern: '^_', + ignoreRestSiblings: true + } + ], '@typescript-eslint/explicit-function-return-type': 'off', - 'react/prop-types': 'off', + 'no-shadow': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -374,7 +379,41 @@ module.exports = { ['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$'] ] } - ] + ], + + // React Hooks + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + // React + 'react/function-component-definition': 'error', + 'react/hook-use-state': 'error', + 'react/jsx-boolean-value': ['error', 'always'], + 'react/jsx-curly-brace-presence': [ + 'error', + { props: 'never', children: 'never' } + ], + 'react/jsx-fragments': 'error', + 'react/jsx-handler-names': [ + 'error', + { + eventHandlerPrefix: 'on', + eventHandlerPropPrefix: 'on' + } + ], + 'react/jsx-no-bind': ['error', { ignoreRefs: true }], + 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], + 'react/jsx-pascal-case': ['error', { allowAllCaps: true }], + 'react/jsx-sort-props': [ + 'error', + { + callbacksLast: true, + noSortAlphabetically: true, + reservedFirst: true + } + ], + 'react/prop-types': 'off', + 'react/self-closing-comp': 'error' }) }, { diff --git a/frontend/babel.config.js b/frontend/babel.config.js index 5c0d5ecdc..ade9f24a2 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -2,6 +2,8 @@ const loose = true; module.exports = { plugins: [ + '@babel/plugin-transform-logical-assignment-operators', + // Stage 1 '@babel/plugin-proposal-export-default-from', ['@babel/plugin-transform-optional-chaining', { loose }], diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 5336d6583..ceacc4f04 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -25,6 +25,7 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', + target: 'web', stats: { children: false @@ -65,7 +66,7 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: '[name]-[contenthash].js', + filename: isProduction ? '[name]-[contenthash].js' : '[name].js', sourceMapFilename: '[file].map' }, @@ -90,7 +91,7 @@ module.exports = (env) => { new MiniCssExtractPlugin({ filename: 'Content/styles.css', - chunkFilename: 'Content/[id]-[chunkhash].css' + chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' }), new HtmlWebpackPlugin({ @@ -169,7 +170,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: 3 + corejs: '3.39' } ] ] @@ -190,7 +191,7 @@ module.exports = (env) => { options: { importLoaders: 1, modules: { - localIdentName: '[name]/[local]/[hash:base64:5]' + localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' } } }, diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index f657adf28..89db00f8c 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,6 +16,7 @@ const mixinsFiles = [ module.exports = { plugins: [ + 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/frontend/src/App/App.js b/frontend/src/App/App.tsx similarity index 57% rename from frontend/src/App/App.js rename to frontend/src/App/App.tsx index 1eea6e082..dba90a697 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.tsx @@ -1,31 +1,30 @@ -import { ConnectedRouter } from 'connected-react-router'; -import PropTypes from 'prop-types'; +import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; import React from 'react'; import DocumentTitle from 'react-document-title'; import { Provider } from 'react-redux'; +import { Store } from 'redux'; import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history }) { +interface AppProps { + store: Store; + history: ConnectedRouterProps['history']; +} + +function App({ store, history }: AppProps) { return ( - - - - - + + + + ); } -App.propTypes = { - store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired -}; - export default App; diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js deleted file mode 100644 index f7a578da2..000000000 --- a/frontend/src/App/AppRoutes.js +++ /dev/null @@ -1,184 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { Redirect, Route } from 'react-router-dom'; -import NotFound from 'Components/NotFound'; -import Switch from 'Components/Router/Switch'; -import HistoryConnector from 'History/HistoryConnector'; -import IndexerIndex from 'Indexer/Index/IndexerIndex'; -import IndexerStats from 'Indexer/Stats/IndexerStats'; -import SearchIndexConnector from 'Search/SearchIndexConnector'; -import ApplicationSettings from 'Settings/Applications/ApplicationSettings'; -import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; -import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; -import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; -import IndexerSettings from 'Settings/Indexers/IndexerSettings'; -import NotificationSettings from 'Settings/Notifications/NotificationSettings'; -import Settings from 'Settings/Settings'; -import TagSettings from 'Settings/Tags/TagSettings'; -import UISettingsConnector from 'Settings/UI/UISettingsConnector'; -import BackupsConnector from 'System/Backup/BackupsConnector'; -import LogsTableConnector from 'System/Events/LogsTableConnector'; -import Logs from 'System/Logs/Logs'; -import Status from 'System/Status/Status'; -import Tasks from 'System/Tasks/Tasks'; -import UpdatesConnector from 'System/Updates/UpdatesConnector'; -import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; - -function AppRoutes(props) { - const { - app - } = props; - - return ( - - {/* - Indexers - */} - - - - { - window.Prowlarr.urlBase && - { - return ( - - ); - }} - /> - } - - - - {/* - Search - */} - - - - {/* - Activity - */} - - - - {/* - Settings - */} - - - - - - - - - - - - - - - - - - - - {/* - System - */} - - - - - - - - - - - - - - {/* - Not Found - */} - - - - ); -} - -AppRoutes.propTypes = { - app: PropTypes.func.isRequired -}; - -export default AppRoutes; diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx new file mode 100644 index 000000000..d451a12fb --- /dev/null +++ b/frontend/src/App/AppRoutes.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import NotFound from 'Components/NotFound'; +import Switch from 'Components/Router/Switch'; +import HistoryConnector from 'History/HistoryConnector'; +import IndexerIndex from 'Indexer/Index/IndexerIndex'; +import IndexerStats from 'Indexer/Stats/IndexerStats'; +import SearchIndexConnector from 'Search/SearchIndexConnector'; +import ApplicationSettings from 'Settings/Applications/ApplicationSettings'; +import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; +import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; +import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; +import IndexerSettings from 'Settings/Indexers/IndexerSettings'; +import NotificationSettings from 'Settings/Notifications/NotificationSettings'; +import Settings from 'Settings/Settings'; +import TagSettings from 'Settings/Tags/TagSettings'; +import UISettingsConnector from 'Settings/UI/UISettingsConnector'; +import BackupsConnector from 'System/Backup/BackupsConnector'; +import LogsTableConnector from 'System/Events/LogsTableConnector'; +import Logs from 'System/Logs/Logs'; +import Status from 'System/Status/Status'; +import Tasks from 'System/Tasks/Tasks'; +import Updates from 'System/Updates/Updates'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; + +function RedirectWithUrlBase() { + return ; +} + +function AppRoutes() { + return ( + + {/* + Indexers + */} + + + + {window.Prowlarr.urlBase && ( + + )} + + + + {/* + Search + */} + + + + {/* + Activity + */} + + + + {/* + Settings + */} + + + + + + + + + + + + + + + + + + + + {/* + System + */} + + + + + + + + + + + + + + {/* + Not Found + */} + + + + ); +} + +export default AppRoutes; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js deleted file mode 100644 index abc7f8832..000000000 --- a/frontend/src/App/AppUpdatedModal.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; - -function AppUpdatedModal(props) { - const { - isOpen, - onModalClose - } = props; - - return ( - - - - ); -} - -AppUpdatedModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModal.tsx b/frontend/src/App/AppUpdatedModal.tsx new file mode 100644 index 000000000..696d36fb2 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +interface AppUpdatedModalProps { + isOpen: boolean; + onModalClose: (...args: unknown[]) => unknown; +} + +function AppUpdatedModal(props: AppUpdatedModalProps) { + const { isOpen, onModalClose } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); + + return ( + + + + ); +} + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js deleted file mode 100644 index a21afbc5a..000000000 --- a/frontend/src/App/AppUpdatedModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import AppUpdatedModal from './AppUpdatedModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(null, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js deleted file mode 100644 index 8cce1bc16..000000000 --- a/frontend/src/App/AppUpdatedModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { kinds } from 'Helpers/Props'; -import UpdateChanges from 'System/Updates/UpdateChanges'; -import translate from 'Utilities/String/translate'; -import styles from './AppUpdatedModalContent.css'; - -function mergeUpdates(items, version, prevVersion) { - let installedIndex = items.findIndex((u) => u.version === version); - let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); - - if (installedIndex === -1) { - installedIndex = 0; - } - - if (installedPreviouslyIndex === -1) { - installedPreviouslyIndex = items.length; - } else if (installedPreviouslyIndex === installedIndex && items.length) { - installedPreviouslyIndex += 1; - } - - const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); - - if (!appliedUpdates.length) { - return null; - } - - const appliedChanges = { new: [], fixed: [] }; - appliedUpdates.forEach((u) => { - if (u.changes) { - appliedChanges.new.push(... u.changes.new); - appliedChanges.fixed.push(... u.changes.fixed); - } - }); - - const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); - - if (!appliedChanges.new.length && !appliedChanges.fixed.length) { - mergedUpdate.changes = null; - } - - return mergedUpdate; -} - -function AppUpdatedModalContent(props) { - const { - version, - prevVersion, - isPopulated, - error, - items, - onSeeChangesPress, - onModalClose - } = props; - - const update = mergeUpdates(items, version, prevVersion); - - return ( - - - {translate('AppUpdated')} - - - -
- -
- - { - isPopulated && !error && !!update && -
- { - !update.changes && -
{translate('MaintenanceRelease')}
- } - - { - !!update.changes && -
-
- {translate('WhatsNew')} -
- - - - -
- } -
- } - - { - !isPopulated && !error && - - } -
- - - - - - -
- ); -} - -AppUpdatedModalContent.propTypes = { - version: PropTypes.string.isRequired, - prevVersion: PropTypes.string, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onSeeChangesPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx new file mode 100644 index 000000000..0bd5df6d3 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +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 { kinds } from 'Helpers/Props'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import Update from 'typings/Update'; +import translate from 'Utilities/String/translate'; +import AppState from './State/AppState'; +import styles from './AppUpdatedModalContent.css'; + +function mergeUpdates(items: Update[], version: string, prevVersion?: string) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex( + (u) => u.version === prevVersion + ); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.length) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges: Update['changes'] = { new: [], fixed: [] }; + + appliedUpdates.forEach((u: Update) => { + if (u.changes) { + appliedChanges.new.push(...u.changes.new); + appliedChanges.fixed.push(...u.changes.fixed); + } + }); + + const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], { + changes: appliedChanges, + }); + + if (!appliedChanges.new.length && !appliedChanges.fixed.length) { + mergedUpdate.changes = null; + } + + return mergedUpdate; +} + +interface AppUpdatedModalContentProps { + onModalClose: () => void; +} + +function AppUpdatedModalContent(props: AppUpdatedModalContentProps) { + const dispatch = useDispatch(); + const { version, prevVersion } = useSelector((state: AppState) => state.app); + const { isPopulated, error, items } = useSelector( + (state: AppState) => state.system.updates + ); + const previousVersion = usePrevious(version); + + const { onModalClose } = props; + + const update = mergeUpdates(items, version, prevVersion); + + const handleSeeChangesPress = useCallback(() => { + window.location.href = `${window.Prowlarr.urlBase}/system/updates`; + }, []); + + useEffect(() => { + dispatch(fetchUpdates()); + }, [dispatch]); + + useEffect(() => { + if (version !== previousVersion) { + dispatch(fetchUpdates()); + } + }, [version, previousVersion, dispatch]); + + return ( + + {translate('AppUpdated')} + + +
+ +
+ + {isPopulated && !error && !!update ? ( +
+ {update.changes ? ( +
+ {translate('MaintenanceRelease')} +
+ ) : null} + + {update.changes ? ( +
+
{translate('WhatsNew')}
+ + + + +
+ ) : null} +
+ ) : null} + + {!isPopulated && !error ? : null} +
+ + + + + + +
+ ); +} + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js deleted file mode 100644 index 97dd0aeb9..000000000 --- a/frontend/src/App/AppUpdatedModalContentConnector.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import AppUpdatedModalContent from './AppUpdatedModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - (state) => state.app.prevVersion, - (state) => state.system.updates, - (version, prevVersion, updates) => { - const { - isPopulated, - error, - items - } = updates; - - return { - version, - prevVersion, - isPopulated, - error, - items - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - dispatchFetchUpdates() { - dispatch(fetchUpdates()); - }, - - onSeeChangesPress() { - window.location = `${window.Prowlarr.urlBase}/system/updates`; - } - }; -} - -class AppUpdatedModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - } - - componentDidUpdate(prevProps) { - if (prevProps.version !== this.props.version) { - this.props.dispatchFetchUpdates(); - } - } - - // - // Render - - render() { - const { - dispatchFetchUpdates, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -AppUpdatedModalContentConnector.propTypes = { - version: PropTypes.string.isRequired, - dispatchFetchUpdates: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ApplyTheme.js b/frontend/src/App/ApplyTheme.js deleted file mode 100644 index bd4d6a6c8..000000000 --- a/frontend/src/App/ApplyTheme.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment, useCallback, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import themes from 'Styles/Themes'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.ui.item.theme || window.Prowlarr.theme, - ( - theme - ) => { - return { - theme - }; - } - ); -} - -function ApplyTheme({ theme, children }) { - // Update the CSS Variables - - const updateCSSVariables = useCallback(() => { - const arrayOfVariableKeys = Object.keys(themes[theme]); - const arrayOfVariableValues = Object.values(themes[theme]); - - // Loop through each array key and set the CSS Variables - arrayOfVariableKeys.forEach((cssVariableKey, index) => { - // Based on our snippet from MDN - document.documentElement.style.setProperty( - `--${cssVariableKey}`, - arrayOfVariableValues[index] - ); - }); - }, [theme]); - - // On Component Mount and Component Update - useEffect(() => { - updateCSSVariables(theme); - }, [updateCSSVariables, theme]); - - return {children}; -} - -ApplyTheme.propTypes = { - theme: PropTypes.string.isRequired, - children: PropTypes.object.isRequired -}; - -export default connect(createMapStateToProps)(ApplyTheme); diff --git a/frontend/src/App/ApplyTheme.tsx b/frontend/src/App/ApplyTheme.tsx new file mode 100644 index 000000000..ec9cd037f --- /dev/null +++ b/frontend/src/App/ApplyTheme.tsx @@ -0,0 +1,33 @@ +import { useCallback, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import themes from 'Styles/Themes'; +import AppState from './State/AppState'; + +function createThemeSelector() { + return createSelector( + (state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme, + (theme) => { + return theme; + } + ); +} + +function ApplyTheme() { + const theme = useSelector(createThemeSelector()); + + const updateCSSVariables = useCallback(() => { + Object.entries(themes[theme]).forEach(([key, value]) => { + document.documentElement.style.setProperty(`--${key}`, value); + }); + }, [theme]); + + // On Component Mount and Component Update + useEffect(() => { + updateCSSVariables(); + }, [updateCSSVariables, theme]); + + return null; +} + +export default ApplyTheme; diff --git a/frontend/src/App/ColorImpairedContext.js b/frontend/src/App/ColorImpairedContext.ts similarity index 100% rename from frontend/src/App/ColorImpairedContext.js rename to frontend/src/App/ColorImpairedContext.ts diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.tsx similarity index 54% rename from frontend/src/App/ConnectionLostModal.js rename to frontend/src/App/ConnectionLostModal.tsx index 5c08f491f..f08f2c0e2 100644 --- a/frontend/src/App/ConnectionLostModal.js +++ b/frontend/src/App/ConnectionLostModal.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback } from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -function ConnectionLostModal(props) { - const { - isOpen, - onModalClose - } = props; +interface ConnectionLostModalProps { + isOpen: boolean; +} + +function ConnectionLostModal(props: ConnectionLostModalProps) { + const { isOpen } = props; + + const handleModalClose = useCallback(() => { + location.reload(); + }, []); return ( - - - - {translate('ConnectionLost')} - + + + {translate('ConnectionLost')} -
- {translate('ConnectionLostToBackend')} -
+
{translate('ConnectionLostToBackend')}
{translate('ConnectionLostReconnect')}
- @@ -48,9 +42,4 @@ function ConnectionLostModal(props) { ); } -ConnectionLostModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js deleted file mode 100644 index 8ab8e3cd0..000000000 --- a/frontend/src/App/ConnectionLostModalConnector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux'; -import ConnectionLostModal from './ConnectionLostModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - location.reload(); - } - }; -} - -export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index d511963fc..f89eb25f7 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,4 +1,6 @@ +import Column from 'Components/Table/Column'; import SortDirection from 'Helpers/Props/SortDirection'; +import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { responseJSON: { @@ -17,7 +19,19 @@ export interface AppSectionSaveState { } export interface PagedAppSectionState { + page: number; pageSize: number; + totalPages: number; + totalRecords?: number; +} +export interface TableAppSectionState { + columns: Column[]; +} + +export interface AppSectionFilterState { + selectedFilterKey: string; + filters: PropertyFilter[]; + filterBuilderProps: FilterBuilderProp[]; } export interface AppSectionSchemaState { @@ -33,6 +47,7 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; + pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 2490f739f..0f0e82c0d 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -42,7 +42,20 @@ export interface CustomFilter { filers: PropertyFilter[]; } +export interface AppSectionState { + isConnected: boolean; + isReconnecting: boolean; + version: string; + prevVersion?: string; + dimensions: { + isSmallScreen: boolean; + width: number; + height: number; + }; +} + interface AppState { + app: AppSectionState; commands: CommandAppState; history: HistoryAppState; indexerHistory: IndexerHistoryAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts index 357ed29f0..3bb0e85f5 100644 --- a/frontend/src/App/State/HistoryAppState.ts +++ b/frontend/src/App/State/HistoryAppState.ts @@ -1,8 +1,12 @@ -import AppSectionState from 'App/State/AppSectionState'; +import AppSectionState, { + AppSectionFilterState, +} from 'App/State/AppSectionState'; import Column from 'Components/Table/Column'; import History from 'typings/History'; -interface HistoryAppState extends AppSectionState { +interface HistoryAppState + extends AppSectionState, + AppSectionFilterState { pageSize: number; columns: Column[]; } diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts index d070986af..4c0145d0d 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -31,6 +31,8 @@ interface IndexerAppState AppSectionDeleteState, AppSectionSaveState { itemMap: Record; + + isTestingAll: boolean; } export type IndexerStatusAppState = AppSectionState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 9dc6dfa2c..33c6c936d 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -3,10 +3,12 @@ import AppSectionState, { AppSectionItemState, AppSectionSaveState, } from 'App/State/AppSectionState'; +import { IndexerCategory } from 'Indexer/Indexer'; import Application from 'typings/Application'; import DownloadClient from 'typings/DownloadClient'; import Notification from 'typings/Notification'; -import { UiSettings } from 'typings/UiSettings'; +import General from 'typings/Settings/General'; +import UiSettings from 'typings/Settings/UiSettings'; export interface AppProfileAppState extends AppSectionState, @@ -22,6 +24,17 @@ export interface ApplicationAppState export interface DownloadClientAppState extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState { + isTestingAll: boolean; +} + +export interface GeneralAppState + extends AppSectionItemState, + AppSectionSaveState {} + +export interface IndexerCategoryAppState + extends AppSectionState, AppSectionDeleteState, AppSectionSaveState {} @@ -35,6 +48,8 @@ interface SettingsAppState { appProfiles: AppProfileAppState; applications: ApplicationAppState; downloadClients: DownloadClientAppState; + general: GeneralAppState; + indexerCategories: IndexerCategoryAppState; notifications: NotificationAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index d43c1d0ee..8bc1b03e2 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -1,10 +1,19 @@ +import Health from 'typings/Health'; import SystemStatus from 'typings/SystemStatus'; -import { AppSectionItemState } from './AppSectionState'; +import Task from 'typings/Task'; +import Update from 'typings/Update'; +import AppSectionState, { AppSectionItemState } from './AppSectionState'; +export type HealthAppState = AppSectionState; export type SystemStatusAppState = AppSectionItemState; +export type TaskAppState = AppSectionState; +export type UpdateAppState = AppSectionState; interface SystemAppState { + health: HealthAppState; status: SystemStatusAppState; + tasks: TaskAppState; + updates: UpdateAppState; } export default SystemAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index 45a5beed7..b9b31bf63 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -12,7 +12,6 @@ export interface CommandBody { lastStartTime: string; trigger: string; suppressMessages: boolean; - seriesId?: number; } interface Command extends ModelBase { diff --git a/frontend/src/Components/Chart/StackedBarChart.js b/frontend/src/Components/Chart/StackedBarChart.js index 3cca1ba81..b69fd8e03 100644 --- a/frontend/src/Components/Chart/StackedBarChart.js +++ b/frontend/src/Components/Chart/StackedBarChart.js @@ -46,6 +46,10 @@ class StackedBarChart extends Component { size: 14, family: defaultFontFamily } + }, + tooltip: { + mode: 'index', + position: 'average' } } }, diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js index aac01a6b5..931557045 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -10,6 +10,7 @@ class DescriptionListItem extends Component { render() { const { + className, titleClassName, descriptionClassName, title, @@ -17,7 +18,7 @@ class DescriptionListItem extends Component { } = this.props; return ( -
+
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component { } DescriptionListItem.propTypes = { + className: PropTypes.string, titleClassName: PropTypes.string, descriptionClassName: PropTypes.string, title: PropTypes.string, diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 022cf5a45..51d286311 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -63,11 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
{info.componentStack}
)} - { -
- Version: {window.Prowlarr.version} -
- } +
Version: {window.Prowlarr.version}
); diff --git a/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx new file mode 100644 index 000000000..6a7dddcfc --- /dev/null +++ b/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import { IndexerCategory } from 'Indexer/Indexer'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +const indexerCategoriesSelector = createSelector( + (state: AppState) => state.settings.indexerCategories, + (categories) => categories.items +); + +function CategoryFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + const categories: IndexerCategory[] = useSelector(indexerCategoriesSelector); + + const tagList = categories.reduce( + (acc: { id: number; name: string }[], element) => { + acc.push({ + id: element.id, + name: `${element.name} (${element.id})`, + }); + + if (element.subCategories && element.subCategories.length > 0) { + element.subCategories.forEach((subCat) => { + acc.push({ + id: subCat.id, + name: `${subCat.name} (${subCat.id})`, + }); + }); + } + + return acc; + }, + [] + ); + + return ; +} + +export default CategoryFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js index 033b9a69a..0c4a31657 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,3 +1,4 @@ +import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = customFilters[customFilters.length -1]; + const last = maxBy(customFilters, 'id'); dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: 'Label is required' + message: translate('LabelIsRequired') } ] }); @@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component { return ( - Custom Filter + {translate('CustomFilter')}
- Label + {translate('Label')}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index ec676f87c..b02844c61 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,10 +3,13 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; +import sortByProp from 'Utilities/Array/sortByProp'; import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; +import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; +import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; @@ -55,9 +58,15 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.BOOL: return BoolFilterBuilderRowValue; + case filterBuilderValueTypes.CATEGORY: + return CategoryFilterBuilderRowValue; + case filterBuilderValueTypes.DATE: return DateFilterBuilderRowValue; + case filterBuilderValueTypes.HISTORY_EVENT_TYPE: + return HistoryEventTypeFilterBuilderRowValue; + case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; @@ -204,7 +213,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort((a, b) => a.value.localeCompare(b.value)); + }).sort(sortByProp('value')); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index a7aed80b6..d1419327a 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByName); + }, []).sort(sortByProp('name')); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts new file mode 100644 index 000000000..5bf9e5785 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts @@ -0,0 +1,16 @@ +import { FilterBuilderProp } from 'App/State/AppState'; + +interface FilterBuilderRowOnChangeProps { + name: string; + value: unknown[]; +} + +interface FilterBuilderRowValueProps { + filterType?: string; + filterValue: string | number | object | string[] | number[] | object[]; + selectedFilterBuilderProp: FilterBuilderProp; + sectionItem: unknown[]; + onChange: (payload: FilterBuilderRowOnChangeProps) => void; +} + +export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx new file mode 100644 index 000000000..03c5f7227 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +const EVENT_TYPE_OPTIONS = [ + { + id: 1, + get name() { + return translate('Grabbed'); + }, + }, + { + id: 3, + get name() { + return translate('IndexerRss'); + }, + }, + { + id: 2, + get name() { + return translate('IndexerQuery'); + }, + }, + { + id: 4, + get name() { + return translate('IndexerAuth'); + }, + }, +]; + +function HistoryEventTypeFilterBuilderRowValue( + props: FilterBuilderRowValueProps +) { + return ; +} + +export default HistoryEventTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 7407f729a..9f378d5a2 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -37,8 +37,8 @@ class CustomFilter extends Component { dispatchSetFilter } = this.props; - // Assume that delete and then unmounting means the delete was successful. - // Moving this check to a ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the deletion was successful. + // Moving this check to an ancestor would be more accurate, but would have // more boilerplate. if (this.state.isDeleting && id === selectedFilterKey) { dispatchSetFilter({ selectedFilterKey: 'all' }); diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 07660426e..99cb6ec5c 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -30,22 +31,24 @@ function CustomFiltersModalContent(props) { { - customFilters.map((customFilter) => { - return ( - - ); - }) + customFilters + .sort((a, b) => sortByProp(a, b, 'label')) + .map((customFilter) => { + return ( + + ); + }) }
diff --git a/frontend/src/Components/Form/AppProfileSelectInputConnector.js b/frontend/src/Components/Form/AppProfileSelectInputConnector.js index 1aef10c30..0ab181e2f 100644 --- a/frontend/src/Components/Form/AppProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/AppProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import SelectInput from './SelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByName), + createSortedSectionSelector('settings.appProfiles', sortByProp('name')), (state, { includeNoChange }) => includeNoChange, (state, { includeMixed }) => includeMixed, (appProfiles, includeNoChange, includeMixed) => { @@ -24,16 +24,20 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - value: translate('NoChange'), - disabled: true + get value() { + return translate('NoChange'); + }, + isDisabled: true }); } if (includeMixed) { values.unshift({ key: 'mixed', - value: '(Mixed)', - disabled: true + get value() { + return `(${translate('Mixed')})`; + }, + isDisabled: true }); } diff --git a/frontend/src/Components/Form/AvailabilitySelectInput.js b/frontend/src/Components/Form/AvailabilitySelectInput.js deleted file mode 100644 index af9bdb2d6..000000000 --- a/frontend/src/Components/Form/AvailabilitySelectInput.js +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import SelectInput from './SelectInput'; - -const availabilityOptions = [ - { key: 'announced', value: 'Announced' }, - { key: 'inCinemas', value: 'In Cinemas' }, - { key: 'released', value: 'Released' }, - { key: 'preDB', value: 'PreDB' } -]; - -function AvailabilitySelectInput(props) { - const values = [...availabilityOptions]; - - const { - includeNoChange, - includeMixed - } = props; - - if (includeNoChange) { - values.unshift({ - key: 'noChange', - value: 'No Change', - disabled: true - }); - } - - if (includeMixed) { - values.unshift({ - key: 'mixed', - value: '(Mixed)', - disabled: true - }); - } - - return ( - - ); -} - -AvailabilitySelectInput.propTypes = { - includeNoChange: PropTypes.bool.isRequired, - includeMixed: PropTypes.bool.isRequired -}; - -AvailabilitySelectInput.defaultProps = { - includeNoChange: false, - includeMixed: false -}; - -export default AvailabilitySelectInput; diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index 162c79885..9cf7a429a 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -3,7 +3,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -21,16 +22,17 @@ function createMapStateToProps() { const values = items .filter((downloadClient) => downloadClient.protocol === protocolFilter) - .sort(sortByName) + .sort(sortByProp('name')) .map((downloadClient) => ({ key: downloadClient.id, - value: downloadClient.name + value: downloadClient.name, + hint: `(${downloadClient.id})` })); if (includeAny) { values.unshift({ key: 0, - value: '(Any)' + value: `(${translate('Any')})` }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index cc4215025..79b1c999c 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import TextInput from './TextInput'; import styles from './EnhancedSelectInput.css'; +const MINIMUM_DISTANCE_FROM_EDGE = 10; + function isArrowKey(keyCode) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component { // Listeners onComputeMaxHeight = (data) => { - const { - top, - bottom - } = data.offsets.reference; - const windowHeight = window.innerHeight; - if ((/^botton/).test(data.placement)) { - data.styles.maxHeight = windowHeight - bottom; - } else { - data.styles.maxHeight = top; - } + data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; return data; }; @@ -271,26 +264,29 @@ class EnhancedSelectInput extends Component { this.setState({ isOpen: !this.state.isOpen }); }; - onSelect = (value) => { - if (Array.isArray(this.props.value)) { - let newValue = null; - const index = this.props.value.indexOf(value); + onSelect = (newValue) => { + const { name, value, values, onChange } = this.props; + + if (Array.isArray(value)) { + let arrayValue = null; + const index = value.indexOf(newValue); + if (index === -1) { - newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); + arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); } else { - newValue = [...this.props.value]; - newValue.splice(index, 1); + arrayValue = [...value]; + arrayValue.splice(index, 1); } - this.props.onChange({ - name: this.props.name, - value: newValue + onChange({ + name, + value: arrayValue }); } else { this.setState({ isOpen: false }); - this.props.onChange({ - name: this.props.name, - value + onChange({ + name, + value: newValue }); } }; @@ -457,6 +453,10 @@ class EnhancedSelectInput extends Component { order: 851, enabled: true, fn: this.onComputeMaxHeight + }, + preventOverflow: { + enabled: true, + boundariesElement: 'viewport' } }} > @@ -485,7 +485,7 @@ class EnhancedSelectInput extends Component { values.map((v, index) => { const hasParent = v.parentKey !== undefined; const depth = hasParent ? 1 : 0; - const parentSelected = hasParent && value.includes(v.parentKey); + const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey); return ( {error.errorMessage} + + { + error.detailedDescription ? + } + tooltip={error.detailedDescription} + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> : + null + } ); }) @@ -39,6 +53,18 @@ function Form(props) { kind={kinds.WARNING} > {warning.errorMessage} + + { + warning.detailedDescription ? + } + tooltip={warning.detailedDescription} + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> : + null + } ); }) diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js deleted file mode 100644 index a7145363a..000000000 --- a/frontend/src/Components/Form/FormInputButton.js +++ /dev/null @@ -1,54 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import { kinds } from 'Helpers/Props'; -import styles from './FormInputButton.css'; - -function FormInputButton(props) { - const { - className, - canSpin, - isLastButton, - ...otherProps - } = props; - - if (canSpin) { - return ( - - ); - } - - return ( -
- - diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 069a4cdf7..6eef54eab 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller'; import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; -import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import HealthStatus from 'System/Status/Health/HealthStatus'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -87,7 +87,7 @@ const links = [ { title: () => translate('Status'), to: '/system/status', - statusComponent: HealthStatusConnector + statusComponent: HealthStatus }, { title: () => translate('Tasks'), diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css index 5e3e3b52c..409062f97 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -24,6 +24,7 @@ composes: link; padding: 10px 24px; + padding-left: 35px; } .isActiveLink { @@ -41,10 +42,6 @@ text-align: center; } -.noIcon { - margin-left: 25px; -} - .status { float: right; } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts index 77e23c767..5bf0eb815 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts @@ -8,7 +8,6 @@ interface CssExports { 'isActiveParentLink': string; 'item': string; 'link': string; - 'noIcon': string; 'status': string; } export const cssExports: CssExports; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js index 754071c79..8d0e4e790 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -63,9 +63,7 @@ class PageSidebarItem extends Component { } - - {typeof title === 'function' ? title() : title} - + {typeof title === 'function' ? title() : title} { !!StatusComponent && diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css index 0b6918296..e9a1b666d 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -22,11 +22,14 @@ display: flex; align-items: center; justify-content: center; + overflow: hidden; height: 24px; } .label { padding: 0 3px; + max-width: 100%; + max-height: 100%; color: var(--toolbarLabelColor); font-size: $extraSmallFontSize; line-height: calc($extraSmallFontSize + 1px); diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js index c93603aa9..675bdfd02 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -23,6 +23,7 @@ function PageToolbarButton(props) { isDisabled && styles.isDisabled )} isDisabled={isDisabled || isSpinning} + title={label} {...otherProps} > { + const section = 'settings.applications'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + handleCommand = (body) => { if (body.action === 'sync') { this.props.dispatchFetchCommands(); @@ -150,8 +160,8 @@ class SignalRConnector extends Component { const resource = body.resource; const status = resource.status; - // Both sucessful and failed commands need to be - // completed, otherwise they spin until they timeout. + // Both successful and failed commands need to be + // completed, otherwise they spin until they time out. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -160,6 +170,16 @@ class SignalRConnector extends Component { } }; + handleDownloadclient = ({ action, resource }) => { + const section = 'settings.downloadClients'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + handleHealth = () => { this.props.dispatchFetchHealth(); }; @@ -168,14 +188,33 @@ class SignalRConnector extends Component { this.props.dispatchFetchIndexerStatus(); }; - handleIndexer = (body) => { - const action = body.action; + handleIndexer = ({ action, resource }) => { const section = 'indexers'; - if (action === 'updated') { - this.props.dispatchUpdateItem({ section, ...body.resource }); + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); } else if (action === 'deleted') { - this.props.dispatchRemoveItem({ section, id: body.resource.id }); + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + + handleIndexerproxy = ({ action, resource }) => { + const section = 'settings.indexerProxies'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); + } + }; + + handleNotification = ({ action, resource }) => { + const section = 'settings.notifications'; + + if (action === 'created' || action === 'updated') { + this.props.dispatchUpdateItem({ section, ...resource }); + } else if (action === 'deleted') { + this.props.dispatchRemoveItem({ section, id: resource.id }); } }; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 31a696df7..24674c3fc 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -2,9 +2,11 @@ import React from 'react'; type PropertyFunction = () => T; +// TODO: Convert to generic so `name` can be a type interface Column { name: string; label: string | PropertyFunction | React.ReactNode; + className?: string; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts new file mode 100644 index 000000000..dfebb2355 --- /dev/null +++ b/frontend/src/Components/Table/usePaging.ts @@ -0,0 +1,54 @@ +import { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +interface PagingOptions { + page: number; + totalPages: number; + gotoPage: ({ page }: { page: number }) => void; +} + +function usePaging(options: PagingOptions) { + const { page, totalPages, gotoPage } = options; + const dispatch = useDispatch(); + + const handleFirstPagePress = useCallback(() => { + dispatch(gotoPage({ page: 1 })); + }, [dispatch, gotoPage]); + + const handlePreviousPagePress = useCallback(() => { + dispatch(gotoPage({ page: Math.max(page - 1, 1) })); + }, [page, dispatch, gotoPage]); + + const handleNextPagePress = useCallback(() => { + dispatch(gotoPage({ page: Math.min(page + 1, totalPages) })); + }, [page, totalPages, dispatch, gotoPage]); + + const handleLastPagePress = useCallback(() => { + dispatch(gotoPage({ page: totalPages })); + }, [totalPages, dispatch, gotoPage]); + + const handlePageSelect = useCallback( + (page: number) => { + dispatch(gotoPage({ page })); + }, + [dispatch, gotoPage] + ); + + return useMemo(() => { + return { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + }; + }, [ + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + ]); +} + +export default usePaging; diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js index f4d4e2af4..fe700b8fe 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,14 +1,15 @@ import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; +import sortByProp from 'Utilities/Array/sortByProp'; import Label from './Label'; import styles from './TagList.css'; function TagList({ tags, tagList }) { const sortedTags = tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) - .filter((t) => t !== undefined) - .sort((a, b) => a.label.localeCompare(b.label)); + .filter((tag) => !!tag) + .sort(sortByProp('label')); return (
diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index bf31501dd..e0f1bf5dc 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,14 +25,3 @@ font-family: 'Ubuntu Mono'; src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype'); } - -/* - * text-security-disc - */ - -@font-face { - font-weight: normal; - font-style: normal; - font-family: 'text-security-disc'; - src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype'); -} diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf deleted file mode 100644 index 86038dba8..000000000 Binary files a/frontend/src/Content/Fonts/text-security-disc.ttf and /dev/null differ diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff deleted file mode 100644 index bc4cc324b..000000000 Binary files a/frontend/src/Content/Fonts/text-security-disc.woff and /dev/null differ diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts new file mode 100644 index 000000000..417db8178 --- /dev/null +++ b/frontend/src/DownloadClient/DownloadProtocol.ts @@ -0,0 +1,3 @@ +type DownloadProtocol = 'usenet' | 'torrent' | 'unknown'; + +export default DownloadProtocol; diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts new file mode 100644 index 000000000..3caf66df2 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useCurrentPage.ts @@ -0,0 +1,9 @@ +import { useHistory } from 'react-router-dom'; + +function useCurrentPage() { + const history = useHistory(); + + return history.action === 'POP'; +} + +export default useCurrentPage; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts new file mode 100644 index 000000000..24cffb2f1 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; + +export default function useModalOpenState( + initialState: boolean +): [boolean, () => void, () => void] { + const [isOpen, setIsOpen] = useState(initialState); + + const setModalOpen = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const setModalClosed = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + return [isOpen, setModalOpen, setModalClosed]; +} diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts new file mode 100644 index 000000000..885c73470 --- /dev/null +++ b/frontend/src/Helpers/Props/TooltipPosition.ts @@ -0,0 +1,3 @@ +type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; + +export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.ts similarity index 100% rename from frontend/src/Helpers/Props/align.js rename to frontend/src/Helpers/Props/align.ts diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 7fed535f2..73ef41956 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -2,9 +2,10 @@ export const BOOL = 'bool'; export const BYTES = 'bytes'; export const DATE = 'date'; export const DEFAULT = 'default'; +export const HISTORY_EVENT_TYPE = 'historyEventType'; export const INDEXER = 'indexer'; export const PROTOCOL = 'protocol'; export const PRIVACY = 'privacy'; export const APP_PROFILE = 'appProfile'; -export const MOVIE_STATUS = 'movieStatus'; +export const CATEGORY = 'category'; export const TAG = 'tag'; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 834452242..773748996 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -43,6 +43,7 @@ import { faChevronCircleRight as fasChevronCircleRight, faChevronCircleUp as fasChevronCircleUp, faCircle as fasCircle, + faCircleDown as fasCircleDown, faCloud as fasCloud, faCloudDownloadAlt as fasCloudDownloadAlt, faCog as fasCog, @@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus; export const CHECK_CIRCLE = fasCheckCircle; export const CHECK_SQUARE = fasSquareCheck; export const CIRCLE = fasCircle; +export const CIRCLE_DOWN = fasCircleDown; export const CIRCLE_OUTLINE = farCircle; export const CLEAR = fasTrashAlt; export const CLIPBOARD = fasCopy; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 6c4564341..f9cd58e6d 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -1,6 +1,5 @@ export const AUTO_COMPLETE = 'autoComplete'; export const APP_PROFILE_SELECT = 'appProfileSelect'; -export const AVAILABILITY_SELECT = 'availabilitySelect'; export const CAPTCHA = 'captcha'; export const CARDIGANNCAPTCHA = 'cardigannCaptcha'; export const CHECK = 'check'; @@ -27,7 +26,6 @@ export const TAG_SELECT = 'tagSelect'; export const all = [ AUTO_COMPLETE, APP_PROFILE_SELECT, - AVAILABILITY_SELECT, CAPTCHA, CARDIGANNCAPTCHA, CHECK, diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.ts similarity index 72% rename from frontend/src/Helpers/Props/kinds.js rename to frontend/src/Helpers/Props/kinds.ts index b0f5ac87f..7ce606716 100644 --- a/frontend/src/Helpers/Props/kinds.js +++ b/frontend/src/Helpers/Props/kinds.ts @@ -7,7 +7,6 @@ export const PRIMARY = 'primary'; export const PURPLE = 'purple'; export const SUCCESS = 'success'; export const WARNING = 'warning'; -export const QUEUE = 'queue'; export const all = [ DANGER, @@ -19,5 +18,15 @@ export const all = [ PURPLE, SUCCESS, WARNING, - QUEUE -]; +] as const; + +export type Kind = + | 'danger' + | 'default' + | 'disabled' + | 'info' + | 'inverse' + | 'primary' + | 'purple' + | 'success' + | 'warning'; diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.ts similarity index 71% rename from frontend/src/Helpers/Props/sizes.js rename to frontend/src/Helpers/Props/sizes.ts index d7f85df5e..ca7a50fbf 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.ts @@ -4,4 +4,6 @@ export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const; + +export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge'; diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js index e0ae06eb1..6d5ab260e 100644 --- a/frontend/src/History/Details/HistoryDetails.js +++ b/frontend/src/History/Details/HistoryDetails.js @@ -3,6 +3,7 @@ import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import Link from 'Components/Link/Link'; +import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; import styles from './HistoryDetails.css'; @@ -10,7 +11,10 @@ function HistoryDetails(props) { const { indexer, eventType, - data + date, + data, + shortDateFormat, + timeFormat } = props; if (eventType === 'indexerQuery' || eventType === 'indexerRss') { @@ -21,7 +25,10 @@ function HistoryDetails(props) { limit, offset, source, - url + host, + url, + elapsedTime, + cached } = data; return ( @@ -86,6 +93,15 @@ function HistoryDetails(props) { null } + { + data ? + : + null + } + { data ? : null } + + { + elapsedTime ? + : + null + } + + { + date ? + : + null + } ); } @@ -101,10 +135,19 @@ function HistoryDetails(props) { if (eventType === 'releaseGrabbed') { const { source, + host, grabTitle, - url + url, + publishedDate, + infoUrl, + downloadClient, + downloadClientName, + elapsedTime, + grabMethod } = data; + const downloadClientNameInfo = downloadClientName ?? downloadClient; + return ( { @@ -125,6 +168,15 @@ function HistoryDetails(props) { null } + { + data ? + : + null + } + { data ? {infoUrl}} + /> : + null + } + + { + publishedDate ? + : + null + } + + { + downloadClientNameInfo ? + : + null + } + { data ? : null } + + { + elapsedTime ? + : + null + } + + { + grabMethod ? + : + null + } + + { + date ? + : + null + } ); } if (eventType === 'indexerAuth') { + const { elapsedTime } = data; + return ( : null } + + { + elapsedTime ? + : + null + } + + { + date ? + : + null + } ); } @@ -171,6 +297,15 @@ function HistoryDetails(props) { title={translate('Name')} data={data.query} /> + + { + date ? + : + null + } ); } @@ -178,6 +313,7 @@ function HistoryDetails(props) { HistoryDetails.propTypes = { indexer: PropTypes.object.isRequired, eventType: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, data: PropTypes.object.isRequired, shortDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js index fbc3114ad..560955de3 100644 --- a/frontend/src/History/Details/HistoryDetailsModal.js +++ b/frontend/src/History/Details/HistoryDetailsModal.js @@ -29,6 +29,7 @@ function HistoryDetailsModal(props) { isOpen, eventType, indexer, + date, data, shortDateFormat, timeFormat, @@ -49,6 +50,7 @@ function HistoryDetailsModal(props) { @@ -193,8 +196,9 @@ History.propTypes = { indexersError: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.string.isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, totalRecords: PropTypes.number, onFilterSelect: PropTypes.func.isRequired, onFirstPagePress: PropTypes.func.isRequired, diff --git a/frontend/src/History/HistoryConnector.js b/frontend/src/History/HistoryConnector.js index cd634ca11..9b29b7d6e 100644 --- a/frontend/src/History/HistoryConnector.js +++ b/frontend/src/History/HistoryConnector.js @@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames'; import withCurrentPage from 'Components/withCurrentPage'; import { executeCommand } from 'Store/Actions/commandActions'; import * as historyActions from 'Store/Actions/historyActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import History from './History'; @@ -14,13 +15,15 @@ function createMapStateToProps() { return createSelector( (state) => state.history, (state) => state.indexers, + createCustomFiltersSelector('history'), createCommandExecutingSelector(commandNames.CLEAR_HISTORY), - (history, indexers, isHistoryClearing) => { + (history, indexers, customFilters, isHistoryClearing) => { return { isIndexersFetching: indexers.isFetching, isIndexersPopulated: indexers.isPopulated, indexersError: indexers.error, isHistoryClearing, + customFilters, ...history }; } diff --git a/frontend/src/History/HistoryEventTypeCell.js b/frontend/src/History/HistoryEventTypeCell.js index b6478eb90..15654d953 100644 --- a/frontend/src/History/HistoryEventTypeCell.js +++ b/frontend/src/History/HistoryEventTypeCell.js @@ -20,21 +20,22 @@ function getIconName(eventType) { } } -function getIconKind(successful) { - switch (successful) { - case false: - return kinds.DANGER; - default: - return kinds.DEFAULT; +function getIconKind(successful, redirect) { + if (redirect) { + return kinds.INFO; + } else if (!successful) { + return kinds.DANGER; } + + return kinds.DEFAULT; } -function getTooltip(eventType, data, indexer) { +function getTooltip(eventType, data, indexer, redirect) { switch (eventType) { case 'indexerQuery': return `Query "${data.query}" sent to ${indexer.name}`; case 'releaseGrabbed': - return `Release grabbed from ${indexer.name}`; + return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`; case 'indexerAuth': return `Auth attempted for ${indexer.name}`; case 'indexerRss': @@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) { } function HistoryEventTypeCell({ eventType, successful, data, indexer }) { + const { grabMethod } = data; + const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect'; + const iconName = getIconName(eventType); - const iconKind = getIconKind(successful); - const tooltip = getTooltip(eventType, data, indexer); + const iconKind = getIconKind(successful, redirect); + const tooltip = getTooltip(eventType, data, indexer, redirect); return ( state.history.items, + (queueItems) => { + return queueItems; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.history.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface HistoryFilterModalProps { + isOpen: boolean; +} + +export default function HistoryFilterModal(props: HistoryFilterModalProps) { + const sectionItems = useSelector(createHistorySelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'history'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + dispatch(setHistoryFilter(payload)); + }, + [dispatch] + ); + + return ( + + ); +} diff --git a/frontend/src/History/HistoryRow.js b/frontend/src/History/HistoryRow.js index f5af8bb11..1d8c38850 100644 --- a/frontend/src/History/HistoryRow.js +++ b/frontend/src/History/HistoryRow.js @@ -257,6 +257,7 @@ class HistoryRow extends Component { key={parameter.key} title={parameter.title} value={data[parameter.key]} + queryType={data.queryType} /> ); } @@ -331,6 +332,21 @@ class HistoryRow extends Component { ); } + if (name === 'host') { + return ( + + { + data.host ? + data.host : + null + } + + ); + } + if (name === 'elapsedTime') { return ( ); } @@ -393,6 +410,7 @@ class HistoryRow extends Component { {value}; } else if (type === 'tmdb') { link = ( - {value} + + {value} + ); } else if (type === 'tvdb') { link = ( diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js deleted file mode 100644 index 9344c8130..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModal.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; -import styles from './AddIndexerModal.css'; - -function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) { - return ( - - - - ); -} - -AddIndexerModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired, - onSelectIndexer: PropTypes.func.isRequired -}; - -export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx new file mode 100644 index 000000000..be22eec57 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModal.tsx @@ -0,0 +1,44 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearIndexerSchema } from 'Store/Actions/indexerActions'; +import AddIndexerModalContent from './AddIndexerModalContent'; +import styles from './AddIndexerModal.css'; + +interface AddIndexerModalProps { + isOpen: boolean; + onSelectIndexer(): void; + onModalClose(): void; +} + +function AddIndexerModal({ + isOpen, + onSelectIndexer, + onModalClose, + ...otherProps +}: AddIndexerModalProps) { + const dispatch = useDispatch(); + + const onModalClosePress = useCallback(() => { + dispatch(clearIndexerSchema()); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css index a58eccfbc..e824c5475 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css @@ -19,12 +19,18 @@ margin-bottom: 16px; } -.alert { +.notice { composes: alert from '~Components/Alert.css'; margin-bottom: 20px; } +.alert { + composes: alert from '~Components/Alert.css'; + + text-align: center; +} + .scroller { flex: 1 1 auto; } diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts index cbedc72a4..5978832e4 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts @@ -10,6 +10,7 @@ interface CssExports { 'indexers': string; 'modalBody': string; 'modalFooter': string; + 'notice': string; 'scroller': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js deleted file mode 100644 index f4050560e..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.js +++ /dev/null @@ -1,324 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput'; -import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; -import TextInput from 'Components/Form/TextInput'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Scroller from 'Components/Scroller/Scroller'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { kinds, scrollDirections } from 'Helpers/Props'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import SelectIndexerRow from './SelectIndexerRow'; -import styles from './AddIndexerModalContent.css'; - -const columns = [ - { - name: 'protocol', - label: () => translate('Protocol'), - isSortable: true, - isVisible: true - }, - { - name: 'sortName', - label: () => translate('Name'), - isSortable: true, - isVisible: true - }, - { - name: 'language', - label: () => translate('Language'), - isSortable: true, - isVisible: true - }, - { - name: 'description', - label: () => translate('Description'), - isSortable: false, - isVisible: true - }, - { - name: 'privacy', - label: () => translate('Privacy'), - isSortable: true, - isVisible: true - }, - { - name: 'categories', - label: () => translate('Categories'), - isSortable: false, - isVisible: true - } -]; - -const protocols = [ - { - key: 'torrent', - value: 'torrent' - }, - { - key: 'usenet', - value: 'nzb' - } -]; - -const privacyLevels = [ - { - key: 'private', - get value() { - return translate('Private'); - } - }, - { - key: 'semiPrivate', - get value() { - return translate('SemiPrivate'); - } - }, - { - key: 'public', - get value() { - return translate('Public'); - } - } -]; - -class AddIndexerModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - filter: '', - filterProtocols: [], - filterLanguages: [], - filterPrivacyLevels: [], - filterCategories: [] - }; - } - - // - // Listeners - - onFilterChange = ({ value }) => { - this.setState({ filter: value }); - }; - - // - // Render - - render() { - const { - indexers, - onIndexerSelect, - sortKey, - sortDirection, - isFetching, - isPopulated, - error, - onSortPress, - onModalClose - } = this.props; - - const languages = Array.from(new Set(indexers.map(({ language }) => language))) - .sort((a, b) => a.localeCompare(b)) - .map((language) => ({ key: language, value: language })); - - const filteredIndexers = indexers.filter((indexer) => { - const { - filter, - filterProtocols, - filterLanguages, - filterPrivacyLevels, - filterCategories - } = this.state; - - if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) { - return false; - } - - if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) { - return false; - } - - if (filterLanguages.length && !filterLanguages.includes(indexer.language)) { - return false; - } - - if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) { - return false; - } - - if (filterCategories.length) { - const { categories = [] } = indexer.capabilities || {}; - const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)]; - const flatCategories = categories - .filter((item) => item.id < 100000) - .flatMap(flat); - - if (!filterCategories.every((item) => flatCategories.includes(item))) { - return false; - } - } - - return true; - }); - - const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers')); - - return ( - - - {translate('AddIndexer')} - - - - - -
-
- - this.setState({ filterProtocols: value })} - /> -
- -
- - this.setState({ filterLanguages: value })} - /> -
- -
- - this.setState({ filterPrivacyLevels: value })} - /> -
- -
- - this.setState({ filterCategories: value })} - /> -
-
- - -
- {translate('ProwlarrSupportsAnyIndexer')} -
-
- - - { - isFetching ? : null - } - { - error ? {errorMessage} : null - } - { - isPopulated && !!indexers.length ? - - - { - filteredIndexers.map((indexer) => ( - - )) - } - -
: - null - } - { - isPopulated && !!indexers.length && !filteredIndexers.length ? - - {translate('NoIndexersFound')} - : - null - } -
-
- - -
- { - isPopulated ? - translate('CountIndexersAvailable', { count: filteredIndexers.length }) : - null - } -
- -
- -
-
-
- ); - } -} - -AddIndexerModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - sortKey: PropTypes.string, - sortDirection: PropTypes.string, - onSortPress: PropTypes.func.isRequired, - indexers: PropTypes.arrayOf(PropTypes.object).isRequired, - onIndexerSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx new file mode 100644 index 000000000..be1413769 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx @@ -0,0 +1,434 @@ +import { some } from 'lodash'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import IndexerAppState from 'App/State/IndexerAppState'; +import Alert from 'Components/Alert'; +import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput'; +import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; +import TextInput from 'Components/Form/TextInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import Scroller from 'Components/Scroller/Scroller'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { kinds, scrollDirections } from 'Helpers/Props'; +import Indexer, { IndexerCategory } from 'Indexer/Indexer'; +import { + fetchIndexerSchema, + selectIndexerSchema, + setIndexerSchemaSort, +} from 'Store/Actions/indexerActions'; +import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { SortCallback } from 'typings/callbacks'; +import sortByProp from 'Utilities/Array/sortByProp'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import SelectIndexerRow from './SelectIndexerRow'; +import styles from './AddIndexerModalContent.css'; + +const COLUMNS = [ + { + name: 'protocol', + label: () => translate('Protocol'), + isSortable: true, + isVisible: true, + }, + { + name: 'sortName', + label: () => translate('Name'), + isSortable: true, + isVisible: true, + }, + { + name: 'language', + label: () => translate('Language'), + isSortable: true, + isVisible: true, + }, + { + name: 'description', + label: () => translate('Description'), + isSortable: false, + isVisible: true, + }, + { + name: 'privacy', + label: () => translate('Privacy'), + isSortable: true, + isVisible: true, + }, + { + name: 'categories', + label: () => translate('Categories'), + isSortable: false, + isVisible: true, + }, +]; + +const PROTOCOLS = [ + { + key: 'torrent', + value: 'torrent', + }, + { + key: 'usenet', + value: 'nzb', + }, +]; + +const PRIVACY_LEVELS = [ + { + key: 'private', + get value() { + return translate('Private'); + }, + }, + { + key: 'semiPrivate', + get value() { + return translate('SemiPrivate'); + }, + }, + { + key: 'public', + get value() { + return translate('Public'); + }, + }, +]; + +interface IndexerSchema extends Indexer { + isExistingIndexer: boolean; +} + +function createAddIndexersSelector() { + return createSelector( + createClientSideCollectionSelector('indexers.schema'), + createAllIndexersSelector(), + (indexers: IndexerAppState, allIndexers) => { + const { isFetching, isPopulated, error, items, sortDirection, sortKey } = + indexers; + + const indexerList: IndexerSchema[] = items.map((item) => { + const { definitionName } = item; + return { + ...item, + isExistingIndexer: some(allIndexers, { definitionName }), + }; + }); + + return { + isFetching, + isPopulated, + error, + indexers: indexerList, + sortKey, + sortDirection, + }; + } + ); +} + +interface AddIndexerModalContentProps { + onSelectIndexer(): void; + onModalClose(): void; +} + +function AddIndexerModalContent(props: AddIndexerModalContentProps) { + const { onSelectIndexer, onModalClose } = props; + + const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } = + useSelector(createAddIndexersSelector()); + const dispatch = useDispatch(); + + const [filter, setFilter] = useState(''); + const [filterProtocols, setFilterProtocols] = useState([]); + const [filterLanguages, setFilterLanguages] = useState([]); + const [filterPrivacyLevels, setFilterPrivacyLevels] = useState([]); + const [filterCategories, setFilterCategories] = useState([]); + + useEffect( + () => { + dispatch(fetchIndexerSchema()); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const onFilterChange = useCallback( + ({ value }: { value: string }) => { + setFilter(value); + }, + [setFilter] + ); + + const onFilterProtocolsChange = useCallback( + ({ value }: { value: string[] }) => { + setFilterProtocols(value); + }, + [setFilterProtocols] + ); + + const onFilterLanguagesChange = useCallback( + ({ value }: { value: string[] }) => { + setFilterLanguages(value); + }, + [setFilterLanguages] + ); + + const onFilterPrivacyLevelsChange = useCallback( + ({ value }: { value: string[] }) => { + setFilterPrivacyLevels(value); + }, + [setFilterPrivacyLevels] + ); + + const onFilterCategoriesChange = useCallback( + ({ value }: { value: number[] }) => { + setFilterCategories(value); + }, + [setFilterCategories] + ); + + const onIndexerSelect = useCallback( + ({ + implementation, + implementationName, + name, + }: { + implementation: string; + implementationName: string; + name: string; + }) => { + dispatch( + selectIndexerSchema({ + implementation, + implementationName, + name, + }) + ); + + onSelectIndexer(); + }, + [dispatch, onSelectIndexer] + ); + + const onSortPress = useCallback( + (sortKey, sortDirection) => { + dispatch(setIndexerSchemaSort({ sortKey, sortDirection })); + }, + [dispatch] + ); + + const languages = useMemo( + () => + Array.from(new Set(indexers.map(({ language }) => language))) + .map((language) => ({ key: language, value: language })) + .sort(sortByProp('value')), + [indexers] + ); + + const filteredIndexers = useMemo(() => { + const flat = ({ + id, + subCategories = [], + }: { + id: number; + subCategories: IndexerCategory[]; + }): number[] => [id, ...subCategories.flatMap(flat)]; + + return indexers.filter((indexer) => { + if ( + filter.length && + !indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && + !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase()) + ) { + return false; + } + + if ( + filterProtocols.length && + !filterProtocols.includes(indexer.protocol) + ) { + return false; + } + + if ( + filterLanguages.length && + !filterLanguages.includes(indexer.language) + ) { + return false; + } + + if ( + filterPrivacyLevels.length && + !filterPrivacyLevels.includes(indexer.privacy) + ) { + return false; + } + + if (filterCategories.length) { + const { categories = [] } = indexer.capabilities || {}; + + const flatCategories = categories + .filter((item) => item.id < 100000) + .flatMap(flat); + + if ( + !filterCategories.every((categoryId) => + flatCategories.includes(categoryId) + ) + ) { + return false; + } + } + + return true; + }); + }, [ + indexers, + filter, + filterProtocols, + filterLanguages, + filterPrivacyLevels, + filterCategories, + ]); + + const errorMessage = getErrorMessage( + error, + translate('UnableToLoadIndexers') + ); + + return ( + + {translate('AddIndexer')} + + + + +
+
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+
+ + +
{translate('ProwlarrSupportsAnyIndexer')}
+
+ + + {isFetching ? : null} + + {error ? ( + + {errorMessage} + + ) : null} + + {isPopulated && !!indexers.length ? ( + + + {filteredIndexers.map((indexer) => ( + + ))} + +
+ ) : null} + + {isPopulated && !!indexers.length && !filteredIndexers.length ? ( + + {translate('NoIndexersFound')} + + ) : null} +
+
+ + +
+ {isPopulated + ? translate('CountIndexersAvailable', { + count: filteredIndexers.length, + }) + : null} +
+ +
+ +
+
+
+ ); +} + +export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js deleted file mode 100644 index a422e0a03..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js +++ /dev/null @@ -1,94 +0,0 @@ -import { some } from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions'; -import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import AddIndexerModalContent from './AddIndexerModalContent'; - -function createMapStateToProps() { - return createSelector( - createClientSideCollectionSelector('indexers.schema'), - createAllIndexersSelector(), - (indexers, allIndexers) => { - const { - isFetching, - isPopulated, - error, - items, - sortDirection, - sortKey - } = indexers; - - const indexerList = items.map((item) => { - const { definitionName } = item; - return { - ...item, - isExistingIndexer: some(allIndexers, { definitionName }) - }; - }); - - return { - isFetching, - isPopulated, - error, - indexers: indexerList, - sortKey, - sortDirection - }; - } - ); -} - -const mapDispatchToProps = { - fetchIndexerSchema, - selectIndexerSchema, - setIndexerSchemaSort -}; - -class AddIndexerModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchIndexerSchema(); - } - - // - // Listeners - - onIndexerSelect = ({ implementation, implementationName, name }) => { - this.props.selectIndexerSchema({ implementation, implementationName, name }); - this.props.onSelectIndexer(); - }; - - onSortPress = (sortKey, sortDirection) => { - this.props.setIndexerSchemaSort({ sortKey, sortDirection }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -AddIndexerModalContentConnector.propTypes = { - fetchIndexerSchema: PropTypes.func.isRequired, - selectIndexerSchema: PropTypes.func.isRequired, - setIndexerSchemaSort: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSelectIndexer: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector); diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx index ab6850573..157050e41 100644 --- a/frontend/src/Indexer/Add/SelectIndexerRow.tsx +++ b/frontend/src/Indexer/Add/SelectIndexerRow.tsx @@ -2,18 +2,19 @@ import React, { useCallback } from 'react'; import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowButton from 'Components/Table/TableRowButton'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { icons } from 'Helpers/Props'; import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel'; +import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel'; import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import { IndexerCapabilities } from 'Indexer/Indexer'; -import firstCharToUpper from 'Utilities/String/firstCharToUpper'; +import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer'; import translate from 'Utilities/String/translate'; import styles from './SelectIndexerRow.css'; interface SelectIndexerRowProps { name: string; - protocol: string; - privacy: string; + protocol: DownloadProtocol; + privacy: IndexerPrivacy; language: string; description: string; capabilities: IndexerCapabilities; @@ -63,7 +64,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) { {description} - {translate(firstCharToUpper(privacy))} + + + diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js index b09b9e656..7dabc50d9 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js @@ -97,7 +97,7 @@ function EditIndexerModalContent(props) { @@ -144,6 +144,7 @@ function EditIndexerModalContent(props) { }) : null } + { ); const [isSelectMode, setIsSelectMode] = useState(false); + useEffect(() => { + dispatch(fetchIndexers()); + dispatch(fetchIndexerStatus()); + }, [dispatch]); + const onAddIndexerPress = useCallback(() => { setIsAddIndexerModalOpen(true); }, [setIsAddIndexerModalOpen]); diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index b0bba5b94..9d42aa389 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -19,6 +19,7 @@ interface SavePayload { seedRatio?: number; seedTime?: number; packSeedTime?: number; + preferMagnetUrl?: boolean; } interface EditIndexerModalContentProps { @@ -35,7 +36,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'true', @@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { const [packSeedTime, setPackSeedTime] = useState( null ); + const [preferMagnetUrl, setPreferMagnetUrl] = useState< + null | string | boolean + >(null); const save = useCallback(() => { let hasChanges = false; @@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { payload.packSeedTime = packSeedTime as number; } + if (preferMagnetUrl !== null) { + hasChanges = true; + payload.preferMagnetUrl = preferMagnetUrl === 'true'; + } + if (hasChanges) { onSavePress(payload); } @@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { seedRatio, seedTime, packSeedTime, + preferMagnetUrl, onSavePress, onModalClose, ]); @@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { case 'packSeedTime': setPackSeedTime(value); break; + case 'preferMagnetUrl': + setPreferMagnetUrl(value); + break; default: console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); } @@ -224,6 +237,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { name="seedRatio" value={seedRatio} helpText={translate('SeedRatioHelpText')} + isFloat={true} onChange={onInputChange} /> @@ -253,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { onChange={onInputChange} /> + + + {translate('PreferMagnetUrl')} + + + diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx index c832806ed..8e30532cc 100644 --- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx +++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx @@ -2,6 +2,7 @@ import { uniqBy } from 'lodash'; import React from 'react'; import Label from 'Components/Label'; import { IndexerCapabilities } from 'Indexer/Indexer'; +import translate from 'Utilities/String/translate'; interface CapabilitiesLabelProps { capabilities: IndexerCapabilities; @@ -38,7 +39,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) { ); })} - {filteredList.length === 0 ? : null} + {filteredList.length === 0 ? : null} ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css index a0a0daee4..a20efded3 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css @@ -11,6 +11,12 @@ flex: 0 0 60px; } +.id { + composes: cell; + + flex: 0 0 60px; +} + .sortName { composes: cell; @@ -23,7 +29,8 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime { +.packSeedTime, +.preferMagnetUrl { composes: cell; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts index c5d22cf6d..42821bd74 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts @@ -8,8 +8,10 @@ interface CssExports { 'cell': string; 'checkInput': string; 'externalLink': string; + 'id': string; 'minimumSeeders': string; 'packSeedTime': string; + 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index d406750ac..e4c3cd32e 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSelect } from 'App/SelectContext'; -import Label from 'Components/Label'; +import CheckInput from 'Components/Form/CheckInput'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; @@ -15,10 +15,10 @@ import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItem import Indexer from 'Indexer/Indexer'; import IndexerTitleLink from 'Indexer/IndexerTitleLink'; import { SelectStateInputProps } from 'typings/props'; -import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import CapabilitiesLabel from './CapabilitiesLabel'; import IndexerStatusCell from './IndexerStatusCell'; +import PrivacyLabel from './PrivacyLabel'; import ProtocolLabel from './ProtocolLabel'; import styles from './IndexerIndexRow.css'; @@ -34,7 +34,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props; const { indexer, appProfile, status, longDateFormat, timeFormat } = - useSelector(createIndexerIndexItemSelector(props.indexerId)); + useSelector(createIndexerIndexItemSelector(indexerId)); const { id, @@ -75,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime') ?.value ?? undefined; + const preferMagnetUrl = + fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl') + ?.value ?? undefined; + const rssUrl = `${window.location.origin}${ window.Prowlarr.urlBase }/${id}/api?apikey=${encodeURIComponent( @@ -103,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { setIsDeleteIndexerModalOpen(false); }, [setIsDeleteIndexerModalOpen]); + const checkInputCallback = useCallback(() => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + }, []); + const onSelectedChange = useCallback( ({ id, value, shiftKey }: SelectStateInputProps) => { selectDispatch({ @@ -148,12 +156,24 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { ); } + if (name === 'id') { + return ( + + + + ); + } + if (name === 'sortName') { return ( @@ -163,7 +183,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { if (name === 'privacy') { return ( - + ); } @@ -266,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { ); } + if (name === 'preferMagnetUrl') { + return ( + + {preferMagnetUrl === undefined ? null : ( + + )} + + ); + } + if (name === 'actions') { return ( columns ); -const Row: React.FC> = ({ - index, - style, - data, -}) => { +function Row({ index, style, data }: ListChildComponentProps) { const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data; if (index >= items.length) { @@ -77,7 +73,7 @@ const Row: React.FC> = ({ />
); -}; +} function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css index 90ad3b0e9..839cd49ff 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css @@ -4,6 +4,12 @@ flex: 0 0 60px; } +.id { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 60px; +} + .sortName { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; @@ -16,7 +22,8 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime { +.packSeedTime, +.preferMagnetUrl { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts index 94d39a9bb..020d61f27 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts @@ -5,8 +5,10 @@ interface CssExports { 'added': string; 'appProfileId': string; 'capabilities': string; + 'id': string; 'minimumSeeders': string; 'packSeedTime': string; + 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx index 6155929df..3aa087790 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -32,19 +32,17 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { ); return ( - - - {translate('ShowSearch')} + + {translate('ShowSearch')} - - - + + ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx index a3d694e9d..1a2350302 100644 --- a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx @@ -8,6 +8,30 @@ import translate from 'Utilities/String/translate'; import DisabledIndexerInfo from './DisabledIndexerInfo'; import styles from './IndexerStatusCell.css'; +function getIconKind(enabled: boolean, redirect: boolean) { + if (enabled) { + return redirect ? kinds.INFO : kinds.SUCCESS; + } + + return kinds.DEFAULT; +} + +function getIconName(enabled: boolean, redirect: boolean) { + if (enabled) { + return redirect ? icons.REDIRECT : icons.CHECK; + } + + return icons.BLOCKLIST; +} + +function getIconTooltip(enabled: boolean, redirect: boolean) { + if (enabled) { + return redirect ? translate('EnabledRedirected') : translate('Enabled'); + } + + return translate('Disabled'); +} + interface IndexerStatusCellProps { className: string; enabled: boolean; @@ -30,22 +54,14 @@ function IndexerStatusCell(props: IndexerStatusCellProps) { ...otherProps } = props; - const enableKind = redirect ? kinds.INFO : kinds.SUCCESS; - const enableIcon = redirect ? icons.REDIRECT : icons.CHECK; - const enableTitle = redirect - ? translate('EnabledRedirected') - : translate('Enabled'); - return ( - { - - } + {status ? ( + {translate(firstCharToUpper(privacy))} + + ); +} + +export default PrivacyLabel; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css b/frontend/src/Indexer/Index/Table/ProtocolLabel.css index 110c7e01c..c94e383b1 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css @@ -11,3 +11,7 @@ border-color: var(--usenetColor); background-color: var(--usenetColor); } + +.unknown { + composes: label from '~Components/Label.css'; +} diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts index f3b389e3d..ba0cb260d 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'torrent': string; + 'unknown': string; 'usenet': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx index 08009109e..c1824452a 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx @@ -1,18 +1,15 @@ import React from 'react'; import Label from 'Components/Label'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import styles from './ProtocolLabel.css'; interface ProtocolLabelProps { - protocol: string; + protocol: DownloadProtocol; } -function ProtocolLabel(props: ProtocolLabelProps) { - const { protocol } = props; - +function ProtocolLabel({ protocol }: ProtocolLabelProps) { const protocolName = protocol === 'usenet' ? 'nzb' : protocol; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ts(7053) return ; } diff --git a/frontend/src/Indexer/Indexer.ts b/frontend/src/Indexer/Indexer.ts index 96a67f446..c363d472c 100644 --- a/frontend/src/Indexer/Indexer.ts +++ b/frontend/src/Indexer/Indexer.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; export interface IndexerStatus extends ModelBase { disabledTill: Date; @@ -24,6 +25,8 @@ export interface IndexerCapabilities extends ModelBase { categories: IndexerCategory[]; } +export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private'; + export interface IndexerField extends ModelBase { order: number; name: string; @@ -36,6 +39,7 @@ export interface IndexerField extends ModelBase { interface Indexer extends ModelBase { name: string; + definitionName: string; description: string; encoding: string; language: string; @@ -46,8 +50,8 @@ interface Indexer extends ModelBase { supportsSearch: boolean; supportsRedirect: boolean; supportsPagination: boolean; - protocol: string; - privacy: string; + protocol: DownloadProtocol; + privacy: IndexerPrivacy; priority: number; fields: IndexerField[]; tags: number[]; diff --git a/frontend/src/Indexer/IndexerTitleLink.tsx b/frontend/src/Indexer/IndexerTitleLink.tsx index 3c22b87f2..be6e32db3 100644 --- a/frontend/src/Indexer/IndexerTitleLink.tsx +++ b/frontend/src/Indexer/IndexerTitleLink.tsx @@ -1,17 +1,16 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import Link from 'Components/Link/Link'; import IndexerInfoModal from './Info/IndexerInfoModal'; import styles from './IndexerTitleLink.css'; interface IndexerTitleLinkProps { - indexerName: string; indexerId: number; + title: string; onCloneIndexerPress(id: number): void; } function IndexerTitleLink(props: IndexerTitleLinkProps) { - const { indexerName, indexerId, onCloneIndexerPress } = props; + const { title, indexerId, onCloneIndexerPress } = props; const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false); @@ -26,7 +25,7 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) { return (
- {indexerName} + {title} ); })}
- + {data.source ? data.source : null} @@ -83,14 +84,15 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) { { - return { - indexer, - }; - } - ); -} - -const tabs = ['details', 'categories', 'history', 'stats']; +const TABS = ['details', 'categories', 'history', 'stats']; interface IndexerInfoModalContentProps { indexerId: number; @@ -50,9 +38,7 @@ interface IndexerInfoModalContentProps { } function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { - const { indexerId, onCloneIndexerPress } = props; - - const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId)); + const { indexerId, onModalClose, onCloneIndexerPress } = props; const { id, @@ -64,54 +50,55 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { fields, tags, protocol, + privacy, capabilities = {} as IndexerCapabilities, - } = indexer as Indexer; + } = useIndexer(indexerId) as Indexer; - const { onModalClose } = props; - - const baseUrl = - fields.find((field) => field.name === 'baseUrl')?.value ?? - (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined); - - const vipExpiration = - fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined; - - const [selectedTab, setSelectedTab] = useState(tabs[0]); + const [selectedTab, setSelectedTab] = useState(TABS[0]); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = useState(false); - const onTabSelect = useCallback( - (index: number) => { - const selectedTab = tabs[index]; + const handleTabSelect = useCallback( + (selectedIndex: number) => { + const selectedTab = TABS[selectedIndex]; setSelectedTab(selectedTab); }, [setSelectedTab] ); - const onEditIndexerPress = useCallback(() => { + const handleEditIndexerPress = useCallback(() => { setIsEditIndexerModalOpen(true); }, [setIsEditIndexerModalOpen]); - const onEditIndexerModalClose = useCallback(() => { + const handleEditIndexerModalClose = useCallback(() => { setIsEditIndexerModalOpen(false); }, [setIsEditIndexerModalOpen]); - const onDeleteIndexerPress = useCallback(() => { + const handleDeleteIndexerPress = useCallback(() => { setIsEditIndexerModalOpen(false); setIsDeleteIndexerModalOpen(true); }, [setIsDeleteIndexerModalOpen]); - const onDeleteIndexerModalClose = useCallback(() => { + const handleDeleteIndexerModalClose = useCallback(() => { setIsDeleteIndexerModalOpen(false); onModalClose(); }, [setIsDeleteIndexerModalOpen, onModalClose]); - const onCloneIndexerPressWrapper = useCallback(() => { + const handleCloneIndexerPressWrapper = useCallback(() => { onCloneIndexerPress(id); onModalClose(); }, [id, onCloneIndexerPress, onModalClose]); + const baseUrl = + fields.find((field) => field.name === 'baseUrl')?.value ?? + (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined); + + const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1'); + + const vipExpiration = + fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined; + return ( {`${name}`} @@ -119,8 +106,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { @@ -160,6 +147,11 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { title={translate('Language')} data={language ?? '-'} /> + : '-'} + /> {vipExpiration ? ( - {baseUrl ? ( - - {baseUrl.replace(/(:\/\/)api\./, '$1')} - + {indexerUrl ? ( + {indexerUrl} ) : ( '-' )} @@ -358,16 +348,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { -
- +
@@ -375,14 +365,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { ); diff --git a/frontend/src/Indexer/NoIndexer.css b/frontend/src/Indexer/NoIndexer.css index 38a01f391..4ad534de3 100644 --- a/frontend/src/Indexer/NoIndexer.css +++ b/frontend/src/Indexer/NoIndexer.css @@ -1,4 +1,6 @@ .message { + composes: alert from '~Components/Alert.css'; + margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Indexer/NoIndexer.tsx b/frontend/src/Indexer/NoIndexer.tsx index 75650cad6..bf5afa1fe 100644 --- a/frontend/src/Indexer/NoIndexer.tsx +++ b/frontend/src/Indexer/NoIndexer.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; @@ -14,11 +15,9 @@ function NoIndexer(props: NoIndexerProps) { if (totalItems > 0) { return ( -
-
- {translate('AllIndexersHiddenDueToFilter')} -
-
+ + {translate('AllIndexersHiddenDueToFilter')} + ); } @@ -29,7 +28,7 @@ function NoIndexer(props: NoIndexerProps) {
-
diff --git a/frontend/src/Indexer/Stats/IndexerStats.tsx b/frontend/src/Indexer/Stats/IndexerStats.tsx index 7d72ca5f1..bccd49cbe 100644 --- a/frontend/src/Indexer/Stats/IndexerStats.tsx +++ b/frontend/src/Indexer/Stats/IndexerStats.tsx @@ -12,8 +12,9 @@ import FilterMenu from 'Components/Menu/FilterMenu'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import { align, kinds } from 'Helpers/Props'; +import { align, icons, kinds } from 'Helpers/Props'; import { fetchIndexerStats, setIndexerStatsFilter, @@ -31,23 +32,30 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal'; import styles from './IndexerStats.css'; function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) { - const data = indexerStats.map((indexer) => { - return { - label: indexer.indexerName, - value: indexer.averageResponseTime, - }; - }); + const statistics = [...indexerStats].sort((a, b) => + a.averageResponseTime === b.averageResponseTime + ? b.averageGrabResponseTime - a.averageGrabResponseTime + : b.averageResponseTime - a.averageResponseTime + ); - data.sort((a, b) => { - return b.value - a.value; - }); - - return data; + return { + labels: statistics.map((indexer) => indexer.indexerName), + datasets: [ + { + label: translate('AverageQueries'), + data: statistics.map((indexer) => indexer.averageResponseTime), + }, + { + label: translate('AverageGrabs'), + data: statistics.map((indexer) => indexer.averageGrabResponseTime), + }, + ], + }; } function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { - const data = indexerStats.map((indexer) => { - return { + const data = [...indexerStats] + .map((indexer) => ({ label: indexer.indexerName, value: (indexer.numberOfFailedQueries + @@ -58,109 +66,102 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) { indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs), - }; - }); + })) + .filter((s) => s.value > 0); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) { - const data = { - labels: indexerStats.map((indexer) => indexer.indexerName), + const statistics = [...indexerStats] + .filter( + (s) => + s.numberOfQueries > 0 || + s.numberOfRssQueries > 0 || + s.numberOfAuthQueries > 0 + ) + .sort( + (a, b) => + b.numberOfQueries + + b.numberOfRssQueries + + b.numberOfAuthQueries - + (a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries) + ); + + return { + labels: statistics.map((indexer) => indexer.indexerName), datasets: [ { label: translate('SearchQueries'), - data: indexerStats.map((indexer) => indexer.numberOfQueries), + data: statistics.map((indexer) => indexer.numberOfQueries), }, { label: translate('RssQueries'), - data: indexerStats.map((indexer) => indexer.numberOfRssQueries), + data: statistics.map((indexer) => indexer.numberOfRssQueries), }, { label: translate('AuthQueries'), - data: indexerStats.map((indexer) => indexer.numberOfAuthQueries), + data: statistics.map((indexer) => indexer.numberOfAuthQueries), }, ], }; - - return data; } function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) { - const data = indexerStats.map((indexer) => { - return { + const data = [...indexerStats] + .map((indexer) => ({ label: indexer.indexerName, value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs, - }; - }); + })) + .filter((s) => s.value > 0); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => { - return { - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfGrabs, - }; - }); + const data = indexerStats.map((indexer) => ({ + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfGrabs, + })); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => { - return { - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfQueries, - }; - }); + const data = indexerStats.map((indexer) => ({ + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfQueries, + })); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } function getHostGrabsData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => { - return { - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfGrabs, - }; - }); + const data = indexerStats.map((indexer) => ({ + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfGrabs, + })); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } function getHostQueryData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => { - return { - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfQueries, - }; - }); + const data = indexerStats.map((indexer) => ({ + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfQueries, + })); - data.sort((a, b) => { - return b.value - a.value; - }); + data.sort((a, b) => b.value - a.value); return data; } @@ -194,6 +195,10 @@ function IndexerStats() { dispatch(fetchIndexerStats()); }, [dispatch]); + const onRefreshPress = useCallback(() => { + dispatch(fetchIndexerStats()); + }, [dispatch]); + const onFilterSelect = useCallback( (value: string) => { dispatch(setIndexerStatsFilter({ selectedFilterKey: value })); @@ -219,17 +224,26 @@ function IndexerStats() { }, 0) ?? 0; return ( - + + + + + @@ -280,7 +294,7 @@ function IndexerStats() {
- state.indexers.itemMap, + (state: AppState) => state.indexers.items, + (itemMap, allIndexers) => { + return indexerId ? allIndexers[itemMap[indexerId]] : undefined; + } + ); +} + +function useIndexer(indexerId?: number) { + return useSelector(createIndexerSelector(indexerId)); +} + +export default useIndexer; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css b/frontend/src/Search/Mobile/SearchIndexOverview.css index 4e184bd0a..e29ff1ef9 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css @@ -47,3 +47,42 @@ $hoverScale: 1.05; right: 0; white-space: nowrap; } + +.downloadLink { + composes: link from '~Components/Link/Link.css'; + + margin: 0 2px; + width: 22px; + color: var(--textColor); + text-align: center; +} + +.manualDownloadContent { + position: relative; + display: inline-block; + margin: 0 2px; + width: 22px; + height: 20.39px; + vertical-align: middle; + line-height: 20.39px; + + &:hover { + color: var(--iconButtonHoverColor); + } +} + +.interactiveIcon { + position: absolute; + top: 4px; + left: 0; + /* width: 100%; */ + text-align: center; +} + +.downloadIcon { + position: absolute; + top: 7px; + left: 8px; + /* width: 100%; */ + text-align: center; +} diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts index 266cf7fca..68256eb25 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts @@ -4,9 +4,13 @@ interface CssExports { 'actions': string; 'container': string; 'content': string; + 'downloadIcon': string; + 'downloadLink': string; 'indexerRow': string; 'info': string; 'infoRow': string; + 'interactiveIcon': string; + 'manualDownloadContent': string; 'title': string; 'titleRow': string; } diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.js b/frontend/src/Search/Mobile/SearchIndexOverview.js deleted file mode 100644 index 1a14ae66c..000000000 --- a/frontend/src/Search/Mobile/SearchIndexOverview.js +++ /dev/null @@ -1,234 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TextTruncate from 'react-text-truncate'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import { icons, kinds } from 'Helpers/Props'; -import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import CategoryLabel from 'Search/Table/CategoryLabel'; -import Peers from 'Search/Table/Peers'; -import dimensions from 'Styles/Variables/dimensions'; -import formatAge from 'Utilities/Number/formatAge'; -import formatBytes from 'Utilities/Number/formatBytes'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './SearchIndexOverview.css'; - -const columnPadding = parseInt(dimensions.movieIndexColumnPadding); -const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); - -function getContentHeight(rowHeight, isSmallScreen) { - const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; - - return rowHeight - padding; -} - -function getDownloadIcon(isGrabbing, isGrabbed, grabError) { - if (isGrabbing) { - return icons.SPINNER; - } else if (isGrabbed) { - return icons.DOWNLOADING; - } else if (grabError) { - return icons.DOWNLOADING; - } - - return icons.DOWNLOAD; -} - -function getDownloadKind(isGrabbed, grabError) { - if (isGrabbed) { - return kinds.SUCCESS; - } - - if (grabError) { - return kinds.DANGER; - } - - return kinds.DEFAULT; -} - -function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { - if (isGrabbing) { - return ''; - } else if (isGrabbed) { - return translate('AddedToDownloadClient'); - } else if (grabError) { - return grabError; - } - - return translate('AddToDownloadClient'); -} - -class SearchIndexOverview extends Component { - - // - // Listeners - - onGrabPress = () => { - const { - guid, - indexerId, - onGrabPress - } = this.props; - - onGrabPress({ - guid, - indexerId - }); - }; - - // - // Render - - render() { - const { - title, - infoUrl, - protocol, - downloadUrl, - magnetUrl, - categories, - seeders, - leechers, - indexerFlags, - size, - age, - ageHours, - ageMinutes, - indexer, - rowHeight, - isSmallScreen, - isGrabbed, - isGrabbing, - grabError - } = this.props; - - const contentHeight = getContentHeight(rowHeight, isSmallScreen); - - return ( -
-
-
-
-
- - - -
- -
- - - { - downloadUrl || magnetUrl ? - : - null - } -
-
-
- {indexer} -
-
- - - { - protocol === 'torrent' && - - } - - - - - - - - { - indexerFlags.length ? - indexerFlags - .sort((a, b) => a.localeCompare(b)) - .map((flag, index) => { - return ( - - ); - }) : - null - } -
-
-
-
- ); - } -} - -SearchIndexOverview.propTypes = { - guid: PropTypes.string.isRequired, - categories: PropTypes.arrayOf(PropTypes.object).isRequired, - protocol: PropTypes.string.isRequired, - age: PropTypes.number.isRequired, - ageHours: PropTypes.number.isRequired, - ageMinutes: PropTypes.number.isRequired, - publishDate: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - infoUrl: PropTypes.string.isRequired, - downloadUrl: PropTypes.string, - magnetUrl: PropTypes.string, - indexerId: PropTypes.number.isRequired, - indexer: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - files: PropTypes.number, - grabs: PropTypes.number, - seeders: PropTypes.number, - leechers: PropTypes.number, - indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired, - rowHeight: PropTypes.number.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - isSmallScreen: PropTypes.bool.isRequired, - onGrabPress: PropTypes.func.isRequired, - isGrabbing: PropTypes.bool.isRequired, - isGrabbed: PropTypes.bool.isRequired, - grabError: PropTypes.string -}; - -SearchIndexOverview.defaultProps = { - isGrabbing: false, - isGrabbed: false -}; - -export default SearchIndexOverview; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.tsx b/frontend/src/Search/Mobile/SearchIndexOverview.tsx new file mode 100644 index 000000000..21a42d70c --- /dev/null +++ b/frontend/src/Search/Mobile/SearchIndexOverview.tsx @@ -0,0 +1,264 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import TextTruncate from 'react-text-truncate'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import { icons, kinds } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +import { IndexerCategory } from 'Indexer/Indexer'; +import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal'; +import CategoryLabel from 'Search/Table/CategoryLabel'; +import Peers from 'Search/Table/Peers'; +import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; +import dimensions from 'Styles/Variables/dimensions'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import styles from './SearchIndexOverview.css'; + +const columnPadding = parseInt(dimensions.movieIndexColumnPadding); +const columnPaddingSmallScreen = parseInt( + dimensions.movieIndexColumnPaddingSmallScreen +); + +function getDownloadIcon( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadKind(isGrabbed: boolean, grabError?: string) { + if (isGrabbed) { + return kinds.SUCCESS; + } + + if (grabError) { + return kinds.DANGER; + } + + return kinds.DEFAULT; +} + +function getDownloadTooltip( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return translate('AddedToDownloadClient'); + } else if (grabError) { + return grabError; + } + + return translate('AddToDownloadClient'); +} + +interface SearchIndexOverviewProps { + guid: string; + protocol: DownloadProtocol; + age: number; + ageHours: number; + ageMinutes: number; + publishDate: string; + title: string; + infoUrl: string; + downloadUrl?: string; + magnetUrl?: string; + indexerId: number; + indexer: string; + categories: IndexerCategory[]; + size: number; + seeders?: number; + leechers?: number; + indexerFlags: string[]; + isGrabbing: boolean; + isGrabbed: boolean; + grabError?: string; + longDateFormat: string; + timeFormat: string; + rowHeight: number; + isSmallScreen: boolean; + onGrabPress(...args: unknown[]): void; +} + +function SearchIndexOverview(props: SearchIndexOverviewProps) { + const { + guid, + indexerId, + protocol, + categories, + age, + ageHours, + ageMinutes, + publishDate, + title, + infoUrl, + downloadUrl, + magnetUrl, + indexer, + size, + seeders, + leechers, + indexerFlags = [], + isGrabbing = false, + isGrabbed = false, + grabError, + longDateFormat, + timeFormat, + rowHeight, + isSmallScreen, + onGrabPress, + } = props; + + const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); + + const { items: downloadClients } = useSelector( + createEnabledDownloadClientsSelector(protocol) + ); + + const onGrabPressWrapper = useCallback(() => { + onGrabPress({ + guid, + indexerId, + }); + }, [guid, indexerId, onGrabPress]); + + const onOverridePress = useCallback(() => { + setIsOverrideModalOpen(true); + }, [setIsOverrideModalOpen]); + + const onOverrideModalClose = useCallback(() => { + setIsOverrideModalOpen(false); + }, [setIsOverrideModalOpen]); + + const contentHeight = useMemo(() => { + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + + return rowHeight - padding; + }, [rowHeight, isSmallScreen]); + + return ( + <> +
+
+
+
+
+ + + +
+ +
+ + + {downloadClients.length > 1 ? ( + +
+ + + +
+ + ) : null} + + {downloadUrl || magnetUrl ? ( + + ) : null} +
+
+
{indexer}
+
+ + + {protocol === 'torrent' && ( + + )} + + + + + + + + {indexerFlags.length + ? indexerFlags + .sort((a, b) => + a.localeCompare(b, undefined, { numeric: true }) + ) + .map((flag, index) => { + return ( + + ); + }) + : null} +
+
+
+
+ + + + ); +} + +export default SearchIndexOverview; diff --git a/frontend/src/Search/NoSearchResults.css b/frontend/src/Search/NoSearchResults.css index eff6272f7..f17dd633e 100644 --- a/frontend/src/Search/NoSearchResults.css +++ b/frontend/src/Search/NoSearchResults.css @@ -1,4 +1,6 @@ .message { + composes: alert from '~Components/Alert.css'; + margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Search/NoSearchResults.tsx b/frontend/src/Search/NoSearchResults.tsx index 4ffd1d7fd..46fbc85e0 100644 --- a/frontend/src/Search/NoSearchResults.tsx +++ b/frontend/src/Search/NoSearchResults.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import Alert from 'Components/Alert'; +import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './NoSearchResults.css'; @@ -11,18 +13,16 @@ function NoSearchResults(props: NoSearchResultsProps) { if (totalItems > 0) { return ( -
-
- {translate('AllIndexersHiddenDueToFilter')} -
-
+ + {translate('AllSearchResultsHiddenByFilter')} + ); } return ( -
-
{translate('NoSearchResultsFound')}
-
+ + {translate('NoSearchResultsFound')} + ); } diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx new file mode 100644 index 000000000..7d623decd --- /dev/null +++ b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import { sizes } from 'Helpers/Props'; +import SelectDownloadClientModalContent from './SelectDownloadClientModalContent'; + +interface SelectDownloadClientModalProps { + isOpen: boolean; + protocol: DownloadProtocol; + modalTitle: string; + onDownloadClientSelect(downloadClientId: number): void; + onModalClose(): void; +} + +function SelectDownloadClientModal(props: SelectDownloadClientModalProps) { + const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } = + props; + + return ( + + + + ); +} + +export default SelectDownloadClientModal; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx new file mode 100644 index 000000000..63e15808f --- /dev/null +++ b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import { kinds } from 'Helpers/Props'; +import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; +import translate from 'Utilities/String/translate'; +import SelectDownloadClientRow from './SelectDownloadClientRow'; + +interface SelectDownloadClientModalContentProps { + protocol: DownloadProtocol; + modalTitle: string; + onDownloadClientSelect(downloadClientId: number): void; + onModalClose(): void; +} + +function SelectDownloadClientModalContent( + props: SelectDownloadClientModalContentProps +) { + const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props; + + const { isFetching, isPopulated, error, items } = useSelector( + createEnabledDownloadClientsSelector(protocol) + ); + + return ( + + + {translate('SelectDownloadClientModalTitle', { modalTitle })} + + + + {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('DownloadClientsLoadError')} + + ) : null} + + {isPopulated && !error ? ( +
+ {items.map((downloadClient) => { + const { id, name, priority } = downloadClient; + + return ( + + ); + })} + + ) : null} +
+ + + + +
+ ); +} + +export default SelectDownloadClientModalContent; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css new file mode 100644 index 000000000..6525db977 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css @@ -0,0 +1,6 @@ +.downloadClient { + display: flex; + justify-content: space-between; + padding: 8px; + border-bottom: 1px solid var(--borderColor); +} diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts new file mode 100644 index 000000000..10c2d3948 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'downloadClient': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx new file mode 100644 index 000000000..6f98d60b4 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx @@ -0,0 +1,32 @@ +import React, { useCallback } from 'react'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; +import styles from './SelectDownloadClientRow.css'; + +interface SelectSeasonRowProps { + id: number; + name: string; + priority: number; + onDownloadClientSelect(downloadClientId: number): unknown; +} + +function SelectDownloadClientRow(props: SelectSeasonRowProps) { + const { id, name, priority, onDownloadClientSelect } = props; + + const onSeasonSelectWrapper = useCallback(() => { + onDownloadClientSelect(id); + }, [id, onDownloadClientSelect]); + + return ( + +
{name}
+
{translate('PrioritySettings', { priority })}
+ + ); +} + +export default SelectDownloadClientRow; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css b/frontend/src/Search/OverrideMatch/OverrideMatchData.css new file mode 100644 index 000000000..bd4d2f788 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchData.css @@ -0,0 +1,17 @@ +.link { + composes: link from '~Components/Link/Link.css'; + + width: 100%; +} + +.placeholder { + display: inline-block; + margin: -2px 0; + width: 100%; + outline: 2px dashed var(--dangerColor); + outline-offset: -2px; +} + +.optional { + outline: 2px dashed var(--gray); +} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts new file mode 100644 index 000000000..dd3ac4575 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'link': string; + 'optional': string; + 'placeholder': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx new file mode 100644 index 000000000..82d6bd812 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx @@ -0,0 +1,35 @@ +import classNames from 'classnames'; +import React from 'react'; +import Link from 'Components/Link/Link'; +import styles from './OverrideMatchData.css'; + +interface OverrideMatchDataProps { + value?: string | number | JSX.Element | JSX.Element[]; + isDisabled?: boolean; + isOptional?: boolean; + onPress: () => void; +} + +function OverrideMatchData(props: OverrideMatchDataProps) { + const { value, isDisabled = false, isOptional, onPress } = props; + + return ( + + {(value == null || (Array.isArray(value) && value.length === 0)) && + !isDisabled ? ( + +   + + ) : ( + value + )} + + ); +} + +export default OverrideMatchData; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx new file mode 100644 index 000000000..16d62ea7c --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import { sizes } from 'Helpers/Props'; +import OverrideMatchModalContent from './OverrideMatchModalContent'; + +interface OverrideMatchModalProps { + isOpen: boolean; + title: string; + indexerId: number; + guid: string; + protocol: DownloadProtocol; + isGrabbing: boolean; + grabError?: string; + onModalClose(): void; +} + +function OverrideMatchModal(props: OverrideMatchModalProps) { + const { + isOpen, + title, + indexerId, + guid, + protocol, + isGrabbing, + grabError, + onModalClose, + } = props; + + return ( + + + + ); +} + +export default OverrideMatchModal; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css new file mode 100644 index 000000000..a5b4b8d52 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css @@ -0,0 +1,49 @@ +.label { + composes: label from '~Components/Label.css'; + + cursor: pointer; +} + +.item { + display: block; + margin-bottom: 5px; + margin-left: 50px; +} + +.footer { + composes: modalFooter from '~Components/Modal/ModalFooter.css'; + + display: flex; + justify-content: space-between; + overflow: hidden; +} + +.error { + margin-right: 20px; + color: var(--dangerColor); + word-break: break-word; +} + +.buttons { + display: flex; +} + +@media only screen and (max-width: $breakpointSmall) { + .item { + margin-left: 0; + } + + .footer { + display: block; + } + + .error { + margin-right: 0; + margin-bottom: 10px; + } + + .buttons { + justify-content: space-between; + flex-grow: 1; + } +} diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts new file mode 100644 index 000000000..79c77d6b5 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts @@ -0,0 +1,11 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'buttons': string; + 'error': string; + 'footer': string; + 'item': string; + 'label': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx new file mode 100644 index 000000000..fbe0ec450 --- /dev/null +++ b/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx @@ -0,0 +1,150 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { grabRelease } from 'Store/Actions/releaseActions'; +import { fetchDownloadClients } from 'Store/Actions/settingsActions'; +import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; +import translate from 'Utilities/String/translate'; +import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal'; +import OverrideMatchData from './OverrideMatchData'; +import styles from './OverrideMatchModalContent.css'; + +type SelectType = 'select' | 'downloadClient'; + +interface OverrideMatchModalContentProps { + indexerId: number; + title: string; + guid: string; + protocol: DownloadProtocol; + isGrabbing: boolean; + grabError?: string; + onModalClose(): void; +} + +function OverrideMatchModalContent(props: OverrideMatchModalContentProps) { + const modalTitle = translate('ManualGrab'); + const { + indexerId, + title, + guid, + protocol, + isGrabbing, + grabError, + onModalClose, + } = props; + + const [downloadClientId, setDownloadClientId] = useState(null); + const [selectModalOpen, setSelectModalOpen] = useState( + null + ); + const previousIsGrabbing = usePrevious(isGrabbing); + + const dispatch = useDispatch(); + const { items: downloadClients } = useSelector( + createEnabledDownloadClientsSelector(protocol) + ); + + const onSelectModalClose = useCallback(() => { + setSelectModalOpen(null); + }, [setSelectModalOpen]); + + const onSelectDownloadClientPress = useCallback(() => { + setSelectModalOpen('downloadClient'); + }, [setSelectModalOpen]); + + const onDownloadClientSelect = useCallback( + (downloadClientId: number) => { + setDownloadClientId(downloadClientId); + setSelectModalOpen(null); + }, + [setDownloadClientId, setSelectModalOpen] + ); + + const onGrabPress = useCallback(() => { + dispatch( + grabRelease({ + indexerId, + guid, + downloadClientId, + }) + ); + }, [indexerId, guid, downloadClientId, dispatch]); + + useEffect(() => { + if (!isGrabbing && previousIsGrabbing) { + onModalClose(); + } + }, [isGrabbing, previousIsGrabbing, onModalClose]); + + useEffect( + () => { + dispatch(fetchDownloadClients()); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + + + {translate('OverrideGrabModalTitle', { title })} + + + + + {downloadClients.length > 1 ? ( + downloadClient.id === downloadClientId + )?.name ?? translate('Default') + } + onPress={onSelectDownloadClientPress} + /> + } + /> + ) : null} + + + + +
{grabError}
+ +
+ + + + {translate('GrabRelease')} + +
+
+ + +
+ ); +} + +export default OverrideMatchModalContent; diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index 5e949fc6e..872328446 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -212,7 +212,11 @@ class SearchFooter extends Component { name="searchQuery" value={searchQuery} buttons={ - + @@ -275,6 +279,7 @@ class SearchFooter extends Component { } + @@ -314,7 +314,7 @@ class SearchIndex extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} - isDisabled={hasNoIndexer} + isDisabled={hasNoSearchResults} onFilterSelect={onFilterSelect} /> diff --git a/frontend/src/Search/SearchIndexConnector.js b/frontend/src/Search/SearchIndexConnector.js index e3302e73c..78a9866b2 100644 --- a/frontend/src/Search/SearchIndexConnector.js +++ b/frontend/src/Search/SearchIndexConnector.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import withScrollPosition from 'Components/withScrollPosition'; import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions'; +import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector'; import SearchIndex from './SearchIndex'; @@ -55,12 +56,20 @@ function createMapDispatchToProps(dispatch, props) { dispatchClearReleases() { dispatch(clearReleases()); + }, + + dispatchFetchDownloadClients() { + dispatch(fetchDownloadClients()); } }; } class SearchIndexConnector extends Component { + componentDidMount() { + this.props.dispatchFetchDownloadClients(); + } + componentWillUnmount() { this.props.dispatchCancelFetchReleases(); this.props.dispatchClearReleases(); @@ -85,6 +94,7 @@ SearchIndexConnector.propTypes = { onBulkGrabPress: PropTypes.func.isRequired, dispatchCancelFetchReleases: PropTypes.func.isRequired, dispatchClearReleases: PropTypes.func.isRequired, + dispatchFetchDownloadClients: PropTypes.func.isRequired, items: PropTypes.arrayOf(PropTypes.object) }; diff --git a/frontend/src/Search/Table/ReleaseLinks.css b/frontend/src/Search/Table/ReleaseLinks.css new file mode 100644 index 000000000..d37a082a1 --- /dev/null +++ b/frontend/src/Search/Table/ReleaseLinks.css @@ -0,0 +1,13 @@ +.links { + margin: 0; +} + +.link { + white-space: nowrap; +} + +.linkLabel { + composes: label from '~Components/Label.css'; + + cursor: pointer; +} diff --git a/frontend/src/Search/Table/ReleaseLinks.css.d.ts b/frontend/src/Search/Table/ReleaseLinks.css.d.ts new file mode 100644 index 000000000..9f91f93a4 --- /dev/null +++ b/frontend/src/Search/Table/ReleaseLinks.css.d.ts @@ -0,0 +1,9 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'link': string; + 'linkLabel': string; + 'links': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Search/Table/ReleaseLinks.tsx b/frontend/src/Search/Table/ReleaseLinks.tsx new file mode 100644 index 000000000..38260bc21 --- /dev/null +++ b/frontend/src/Search/Table/ReleaseLinks.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import { kinds, sizes } from 'Helpers/Props'; +import { IndexerCategory } from 'Indexer/Indexer'; +import styles from './ReleaseLinks.css'; + +interface ReleaseLinksProps { + categories: IndexerCategory[]; + imdbId?: string; + tmdbId?: number; + tvdbId?: number; + tvMazeId?: number; +} + +function ReleaseLinks(props: ReleaseLinksProps) { + const { categories = [], imdbId, tmdbId, tvdbId, tvMazeId } = props; + + const categoryNames = categories + .filter((item) => item.id < 100000) + .map((c) => c.name); + + return ( +
+ {imdbId ? ( + + + + ) : null} + + {tmdbId ? ( + + + + ) : null} + + {tvdbId ? ( + + + + ) : null} + + {tvMazeId ? ( + + + + ) : null} +
+ ); +} + +export default ReleaseLinks; diff --git a/frontend/src/Search/Table/SearchIndexItemConnector.js b/frontend/src/Search/Table/SearchIndexItemConnector.js index 490214529..4cc7fb20c 100644 --- a/frontend/src/Search/Table/SearchIndexItemConnector.js +++ b/frontend/src/Search/Table/SearchIndexItemConnector.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { executeCommand } from 'Store/Actions/commandActions'; function createReleaseSelector() { return createSelector( @@ -37,10 +36,6 @@ function createMapStateToProps() { ); } -const mapDispatchToProps = { - dispatchExecuteCommand: executeCommand -}; - class SearchIndexItemConnector extends Component { // @@ -71,4 +66,4 @@ SearchIndexItemConnector.propTypes = { component: PropTypes.elementType.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector); +export default connect(createMapStateToProps, null)(SearchIndexItemConnector); diff --git a/frontend/src/Search/Table/SearchIndexRow.css b/frontend/src/Search/Table/SearchIndexRow.css index 342092b81..b36ec4071 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css +++ b/frontend/src/Search/Table/SearchIndexRow.css @@ -63,7 +63,37 @@ } .externalLinks { + composes: button from '~Components/Link/IconButton.css'; + + color: var(--textColor); +} + +.manualDownloadContent { + position: relative; + display: inline-block; margin: 0 2px; width: 22px; + height: 20.39px; + vertical-align: middle; + line-height: 20.39px; + + &:hover { + color: var(--iconButtonHoverColor); + } +} + +.interactiveIcon { + position: absolute; + top: 4px; + left: 0; + /* width: 100%; */ + text-align: center; +} + +.downloadIcon { + position: absolute; + top: 7px; + left: 8px; + /* width: 100%; */ text-align: center; } diff --git a/frontend/src/Search/Table/SearchIndexRow.css.d.ts b/frontend/src/Search/Table/SearchIndexRow.css.d.ts index 6d625f58a..7552b96f9 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css.d.ts +++ b/frontend/src/Search/Table/SearchIndexRow.css.d.ts @@ -6,12 +6,15 @@ interface CssExports { 'category': string; 'cell': string; 'checkInput': string; + 'downloadIcon': string; 'downloadLink': string; 'externalLinks': string; 'files': string; 'grabs': string; 'indexer': string; 'indexerFlags': string; + 'interactiveIcon': string; + 'manualDownloadContent': string; 'peers': string; 'protocol': string; 'size': string; diff --git a/frontend/src/Search/Table/SearchIndexRow.js b/frontend/src/Search/Table/SearchIndexRow.js deleted file mode 100644 index 67c267696..000000000 --- a/frontend/src/Search/Table/SearchIndexRow.js +++ /dev/null @@ -1,396 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import Link from 'Components/Link/Link'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; -import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; -import Popover from 'Components/Tooltip/Popover'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatAge from 'Utilities/Number/formatAge'; -import formatBytes from 'Utilities/Number/formatBytes'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import CategoryLabel from './CategoryLabel'; -import Peers from './Peers'; -import styles from './SearchIndexRow.css'; - -function getDownloadIcon(isGrabbing, isGrabbed, grabError) { - if (isGrabbing) { - return icons.SPINNER; - } else if (isGrabbed) { - return icons.DOWNLOADING; - } else if (grabError) { - return icons.DOWNLOADING; - } - - return icons.DOWNLOAD; -} - -function getDownloadKind(isGrabbed, grabError) { - if (isGrabbed) { - return kinds.SUCCESS; - } - - if (grabError) { - return kinds.DANGER; - } - - return kinds.DEFAULT; -} - -function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { - if (isGrabbing) { - return ''; - } else if (isGrabbed) { - return translate('AddedToDownloadClient'); - } else if (grabError) { - return grabError; - } - - return translate('AddToDownloadClient'); -} - -class SearchIndexRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isConfirmGrabModalOpen: false - }; - } - - // - // Listeners - - onGrabPress = () => { - const { - guid, - indexerId, - onGrabPress - } = this.props; - - onGrabPress({ - guid, - indexerId - }); - }; - - onSavePress = () => { - const { - downloadUrl, - fileName, - onSavePress - } = this.props; - - onSavePress({ - downloadUrl, - fileName - }); - }; - - // - // Render - - render() { - const { - guid, - protocol, - downloadUrl, - magnetUrl, - categories, - age, - ageHours, - ageMinutes, - publishDate, - title, - infoUrl, - indexer, - size, - files, - grabs, - seeders, - leechers, - indexerFlags, - columns, - isGrabbing, - isGrabbed, - grabError, - longDateFormat, - timeFormat, - isSelected, - onSelectedChange - } = this.props; - - return ( - <> - { - columns.map((column) => { - const { - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (column.name === 'select') { - return ( - - ); - } - - if (column.name === 'protocol') { - return ( - - - - ); - } - - if (column.name === 'age') { - return ( - - {formatAge(age, ageHours, ageMinutes)} - - ); - } - - if (column.name === 'sortTitle') { - return ( - - -
- {title} -
- -
- ); - } - - if (column.name === 'indexer') { - return ( - - {indexer} - - ); - } - - if (column.name === 'size') { - return ( - - {formatBytes(size)} - - ); - } - - if (column.name === 'files') { - return ( - - {files} - - ); - } - - if (column.name === 'grabs') { - return ( - - {grabs} - - ); - } - - if (column.name === 'peers') { - return ( - - { - protocol === 'torrent' && - - } - - ); - } - - if (column.name === 'category') { - return ( - - - - ); - } - - if (column.name === 'indexerFlags') { - return ( - - { - !!indexerFlags.length && - - } - title={translate('IndexerFlags')} - body={ -
    - { - indexerFlags.map((flag, index) => { - return ( -
  • - {titleCase(flag)} -
  • - ); - }) - } -
- } - position={tooltipPositions.LEFT} - /> - } -
- ); - } - - if (column.name === 'actions') { - return ( - - - - { - downloadUrl ? - : - null - } - - { - magnetUrl ? - : - null - } - - ); - } - - return null; - }) - } - - ); - } -} - -SearchIndexRow.propTypes = { - guid: PropTypes.string.isRequired, - categories: PropTypes.arrayOf(PropTypes.object).isRequired, - protocol: PropTypes.string.isRequired, - age: PropTypes.number.isRequired, - ageHours: PropTypes.number.isRequired, - ageMinutes: PropTypes.number.isRequired, - publishDate: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - fileName: PropTypes.string.isRequired, - infoUrl: PropTypes.string.isRequired, - downloadUrl: PropTypes.string, - magnetUrl: PropTypes.string, - indexerId: PropTypes.number.isRequired, - indexer: PropTypes.string.isRequired, - size: PropTypes.number.isRequired, - files: PropTypes.number, - grabs: PropTypes.number, - seeders: PropTypes.number, - leechers: PropTypes.number, - indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onGrabPress: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - isGrabbing: PropTypes.bool.isRequired, - isGrabbed: PropTypes.bool.isRequired, - grabError: PropTypes.string, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - isSelected: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired -}; - -SearchIndexRow.defaultProps = { - isGrabbing: false, - isGrabbed: false -}; - -export default SearchIndexRow; diff --git a/frontend/src/Search/Table/SearchIndexRow.tsx b/frontend/src/Search/Table/SearchIndexRow.tsx new file mode 100644 index 000000000..1136a7f64 --- /dev/null +++ b/frontend/src/Search/Table/SearchIndexRow.tsx @@ -0,0 +1,395 @@ +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import Column from 'Components/Table/Column'; +import Popover from 'Components/Tooltip/Popover'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +import { IndexerCategory } from 'Indexer/Indexer'; +import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal'; +import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector'; +import { SelectStateInputProps } from 'typings/props'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import CategoryLabel from './CategoryLabel'; +import Peers from './Peers'; +import ReleaseLinks from './ReleaseLinks'; +import styles from './SearchIndexRow.css'; + +function getDownloadIcon( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadKind(isGrabbed: boolean, grabError?: string) { + if (isGrabbed) { + return kinds.SUCCESS; + } + + if (grabError) { + return kinds.DANGER; + } + + return kinds.DEFAULT; +} + +function getDownloadTooltip( + isGrabbing: boolean, + isGrabbed: boolean, + grabError?: string +) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return translate('AddedToDownloadClient'); + } else if (grabError) { + return grabError; + } + + return translate('AddToDownloadClient'); +} + +interface SearchIndexRowProps { + guid: string; + protocol: DownloadProtocol; + age: number; + ageHours: number; + ageMinutes: number; + publishDate: string; + title: string; + fileName: string; + infoUrl: string; + downloadUrl?: string; + magnetUrl?: string; + indexerId: number; + indexer: string; + categories: IndexerCategory[]; + size: number; + files?: number; + grabs?: number; + seeders?: number; + leechers?: number; + imdbId?: string; + tmdbId?: number; + tvdbId?: number; + tvMazeId?: number; + indexerFlags: string[]; + isGrabbing: boolean; + isGrabbed: boolean; + grabError?: string; + longDateFormat: string; + timeFormat: string; + columns: Column[]; + isSelected?: boolean; + onSelectedChange(result: SelectStateInputProps): void; + onGrabPress(...args: unknown[]): void; + onSavePress(...args: unknown[]): void; +} + +function SearchIndexRow(props: SearchIndexRowProps) { + const { + guid, + indexerId, + protocol, + categories, + age, + ageHours, + ageMinutes, + publishDate, + title, + fileName, + infoUrl, + downloadUrl, + magnetUrl, + indexer, + size, + files, + grabs, + seeders, + leechers, + imdbId, + tmdbId, + tvdbId, + tvMazeId, + indexerFlags = [], + isGrabbing = false, + isGrabbed = false, + grabError, + longDateFormat, + timeFormat, + columns, + isSelected, + onSelectedChange, + onGrabPress, + onSavePress, + } = props; + + const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false); + + const { items: downloadClients } = useSelector( + createEnabledDownloadClientsSelector(protocol) + ); + + const onGrabPressWrapper = useCallback(() => { + onGrabPress({ + guid, + indexerId, + }); + }, [guid, indexerId, onGrabPress]); + + const onSavePressWrapper = useCallback(() => { + onSavePress({ + downloadUrl, + fileName, + }); + }, [downloadUrl, fileName, onSavePress]); + + const onOverridePress = useCallback(() => { + setIsOverrideModalOpen(true); + }, [setIsOverrideModalOpen]); + + const onOverrideModalClose = useCallback(() => { + setIsOverrideModalOpen(false); + }, [setIsOverrideModalOpen]); + + return ( + <> + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'select') { + return ( + + ); + } + + if (name === 'protocol') { + return ( + + + + ); + } + + if (name === 'age') { + return ( + + {formatAge(age, ageHours, ageMinutes)} + + ); + } + + if (name === 'sortTitle') { + return ( + + +
{title}
+ +
+ ); + } + + if (name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (name === 'size') { + return ( + + {formatBytes(size)} + + ); + } + + if (name === 'files') { + return ( + + {files} + + ); + } + + if (name === 'grabs') { + return ( + + {grabs} + + ); + } + + if (name === 'peers') { + return ( + + {protocol === 'torrent' && ( + + )} + + ); + } + + if (name === 'category') { + return ( + + + + ); + } + + if (name === 'indexerFlags') { + return ( + + {!!indexerFlags.length && ( + } + title={translate('IndexerFlags')} + body={ +
    + {indexerFlags.map((flag, index) => { + return
  • {titleCase(flag)}
  • ; + })} +
+ } + position={tooltipPositions.LEFT} + /> + )} +
+ ); + } + + if (name === 'actions') { + return ( + + + + {downloadClients.length > 1 ? ( + +
+ + + +
+ + ) : null} + + {downloadUrl ? ( + + ) : null} + + {magnetUrl ? ( + + ) : null} + + {imdbId || tmdbId || tvdbId || tvMazeId ? ( + + } + title={translate('Links')} + body={ + + } + position={tooltipPositions.TOP} + /> + ) : null} +
+ ); + } + + return null; + })} + + + + ); +} + +export default SearchIndexRow; diff --git a/frontend/src/Settings/Applications/ApplicationSettings.tsx b/frontend/src/Settings/Applications/ApplicationSettings.tsx index c35d55e2d..7fc4b1d7b 100644 --- a/frontend/src/Settings/Applications/ApplicationSettings.tsx +++ b/frontend/src/Settings/Applications/ApplicationSettings.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import { APP_INDEXER_SYNC } from 'Commands/commandNames'; @@ -56,7 +56,7 @@ function ApplicationSettings() { // @ts-ignore showSave={false} additionalButtons={ - + <> - + } /> diff --git a/frontend/src/Settings/Applications/Applications/Application.js b/frontend/src/Settings/Applications/Applications/Application.js index 610cc344d..086d39ee1 100644 --- a/frontend/src/Settings/Applications/Applications/Application.js +++ b/frontend/src/Settings/Applications/Applications/Application.js @@ -57,6 +57,7 @@ class Application extends Component { const { id, name, + enable, syncLevel, fields, tags, @@ -77,7 +78,7 @@ class Application extends Component {
{ - applicationUrl ? + enable && applicationUrl ?
diff --git a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js index bbf8722c5..9f5e570c5 100644 --- a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js +++ b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Applications from './Applications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.applications', sortByName), + createSortedSectionSelector('settings.applications', sortByProp('name')), createTagsSelector(), (applications, tagList) => { return { diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx index 10e73b52a..57e88a4fe 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx @@ -30,7 +30,7 @@ const syncLevelOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: ApplicationSyncLevel.Disabled, diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx index 8c45cf8a5..bb81729f3 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx @@ -14,9 +14,11 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; import { bulkDeleteApplications, bulkEditApplications, + setManageApplicationsSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { SelectStateInputProps } from 'typings/props'; @@ -62,6 +64,8 @@ const COLUMNS = [ interface ManageApplicationsModalContentProps { onModalClose(): void; + sortKey?: string; + sortDirection?: SortDirection; } function ManageApplicationsModalContent( @@ -76,6 +80,8 @@ function ManageApplicationsModalContent( isSaving, error, items, + sortKey, + sortDirection, }: ApplicationAppState = useSelector( createClientSideCollectionSelector('settings.applications') ); @@ -96,6 +102,13 @@ function ManageApplicationsModalContent( const selectedCount = selectedIds.length; + const onSortPress = useCallback( + (value: string) => { + dispatch(setManageApplicationsSort({ sortKey: value })); + }, + [dispatch] + ); + const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -200,7 +213,10 @@ function ManageApplicationsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + sortKey={sortKey} + sortDirection={sortDirection} onSelectAllChange={onSelectAllChange} + onSortPress={onSortPress} > {items.map((item) => { @@ -252,9 +268,9 @@ function ManageApplicationsModalContent( diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js index 640d56a89..51f390d4f 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; +import { icons, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import AddDownloadClientModal from './AddDownloadClientModal'; import DownloadClient from './DownloadClient'; @@ -59,48 +60,59 @@ class DownloadClients extends Component { } = this.state; return ( -
- -
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- -
-
+
+ +
+ {translate('ProwlarrDownloadClientsAlert')}
+
+ {translate('ProwlarrDownloadClientsInAppOnlyAlert')} +
+
- +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } - - -
+ +
+ +
+
+
+ + + + + +
+
); } } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 9cba9c1cc..4f6833fcb 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByName), + createSortedSectionSelector('settings.downloadClients', sortByProp('name')), (downloadClients) => downloadClients ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index b4dd3c1e9..c57432710 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -17,6 +17,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { icons, inputTypes, kinds } from 'Helpers/Props'; +import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import AddCategoryModalConnector from './Categories/AddCategoryModalConnector'; import Category from './Categories/Category'; @@ -61,6 +62,7 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteDownloadClientPress, onConfirmDeleteCategory, ...otherProps @@ -219,6 +221,12 @@ class EditDownloadClientModalContent extends Component { } + + { + this.props.toggleAdvancedSettings(); + }; + onConfirmDeleteCategory = (id) => { this.props.deleteDownloadClientCategory({ id }); }; @@ -81,6 +94,7 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} onConfirmDeleteCategory={this.onConfirmDeleteCategory} @@ -102,6 +116,7 @@ EditDownloadClientModalContentConnector.propTypes = { setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index a2e0f89c6..d18e694c9 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -30,7 +30,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 8e257ae7a..fa82d61b9 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -14,9 +14,11 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; +import SortDirection from 'Helpers/Props/SortDirection'; import { bulkDeleteDownloadClients, bulkEditDownloadClients, + setManageDownloadClientsSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { SelectStateInputProps } from 'typings/props'; @@ -61,6 +63,8 @@ const COLUMNS = [ interface ManageDownloadClientsModalContentProps { onModalClose(): void; + sortKey?: string; + sortDirection?: SortDirection; } function ManageDownloadClientsModalContent( @@ -75,6 +79,8 @@ function ManageDownloadClientsModalContent( isSaving, error, items, + sortKey, + sortDirection, }: DownloadClientAppState = useSelector( createClientSideCollectionSelector('settings.downloadClients') ); @@ -93,6 +99,13 @@ function ManageDownloadClientsModalContent( const selectedCount = selectedIds.length; + const onSortPress = useCallback( + (value: string) => { + dispatch(setManageDownloadClientsSort({ sortKey: value })); + }, + [dispatch] + ); + const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -173,7 +186,10 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} + sortKey={sortKey} + sortDirection={sortDirection} onSelectAllChange={onSelectAllChange} + onSortPress={onSortPress} > {items.map((item) => { @@ -217,9 +233,9 @@ function ManageDownloadClientsModalContent( diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 540e29b01..61a259258 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -15,12 +15,14 @@ const logLevelOptions = [ function LoggingSettings(props) { const { + advancedSettings, settings, onInputChange } = props; const { - logLevel + logLevel, + logSizeLimit } = settings; return ( @@ -37,11 +39,30 @@ function LoggingSettings(props) { {...logLevel} /> + + + {translate('LogSizeLimit')} + + + ); } LoggingSettings.propTypes = { + advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 3bf8d43b6..9cf1b7932 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -12,7 +12,6 @@ function UpdateSettings(props) { const { advancedSettings, settings, - isWindows, packageUpdateMechanism, onInputChange } = props; @@ -38,10 +37,10 @@ function UpdateSettings(props) { value: titleCase(packageUpdateMechanism) }); } else { - updateOptions.push({ key: 'builtIn', value: 'Built-In' }); + updateOptions.push({ key: 'builtIn', value: translate('BuiltIn') }); } - updateOptions.push({ key: 'script', value: 'Script' }); + updateOptions.push({ key: 'script', value: translate('Script') }); return (
@@ -62,61 +61,58 @@ function UpdateSettings(props) { /> - { - !isWindows && -
- - {translate('Automatic')} +
+ + {translate('Automatic')} - - + + + + {translate('Mechanism')} + + + + + { + updateMechanism.value === 'script' && - {translate('Mechanism')} + {translate('ScriptPath')} - - { - updateMechanism.value === 'script' && - - {translate('ScriptPath')} - - - - } -
- } + } +
); } diff --git a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js index 8579c2a2f..59e10422b 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js @@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import styles from './EditIndexerProxyModalContent.css'; @@ -31,6 +32,7 @@ function EditIndexerProxyModalContent(props) { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteIndexerProxyPress, ...otherProps } = props; @@ -130,6 +132,12 @@ function EditIndexerProxyModalContent(props) { } + + { + this.props.toggleAdvancedSettings(); + }; + // // Render @@ -65,6 +76,7 @@ class EditIndexerProxyModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -82,6 +94,7 @@ EditIndexerProxyModalContentConnector.propTypes = { setIndexerProxyFieldValue: PropTypes.func.isRequired, saveIndexerProxy: PropTypes.func.isRequired, testIndexerProxy: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js index 9d2188a7c..0d2acae87 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxiesConnector.js @@ -5,13 +5,13 @@ import { createSelector } from 'reselect'; import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import IndexerProxies from './IndexerProxies'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexerProxies', sortByName), - createSortedSectionSelector('indexers', sortByName), + createSortedSectionSelector('settings.indexerProxies', sortByProp('name')), + createSortedSectionSelector('indexers', sortByProp('name')), createTagsSelector(), (indexerProxies, indexers, tagList) => { return { diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index 60e368617..ed00d96e6 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import NotificationEventItems from './NotificationEventItems'; import styles from './EditNotificationModalContent.css'; @@ -32,6 +33,7 @@ function EditNotificationModalContent(props) { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteNotificationPress, ...otherProps } = props; @@ -136,6 +138,12 @@ function EditNotificationModalContent(props) { } + + { + this.props.toggleAdvancedSettings(); + }; + // // Render @@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = { setNotificationFieldValue: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index b306f742a..6351c6f8a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByName), + createSortedSectionSelector('settings.notifications', sortByProp('name')), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js index 4cb83e8f6..213445c65 100644 --- a/frontend/src/Settings/PendingChangesModal.js +++ b/frontend/src/Settings/PendingChangesModal.js @@ -15,12 +15,17 @@ function PendingChangesModal(props) { isOpen, onConfirm, onCancel, - bindShortcut + bindShortcut, + unbindShortcut } = props; useEffect(() => { - bindShortcut('enter', onConfirm); - }, [bindShortcut, onConfirm]); + if (isOpen) { + bindShortcut('enter', onConfirm); + + return () => unbindShortcut('enter', onConfirm); + } + }, [bindShortcut, unbindShortcut, isOpen, onConfirm]); return ( - {translate('RSS')} + {translate('Rss')} } diff --git a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js index a150655a6..02bf845df 100644 --- a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js +++ b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import AppProfiles from './AppProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByName), + createSortedSectionSelector('settings.appProfiles', sortByProp('name')), (appProfiles) => appProfiles ); } diff --git a/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js b/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js index aace8e039..ac67c77f2 100644 --- a/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js +++ b/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js @@ -97,20 +97,6 @@ class EditAppProfileModalContent extends Component { /> - - - {translate('EnableInteractiveSearch')} - - - - - {translate('EnableAutomaticSearch')} @@ -125,6 +111,20 @@ class EditAppProfileModalContent extends Component { /> + + + {translate('EnableInteractiveSearch')} + + + + + {translate('MinimumSeeders')} diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 4f311e984..1f3de2034 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,12 +3,14 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions'; -import { fetchTagDetails } from 'Store/Actions/tagActions'; +import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - (state) => state.tags, + createSortedSectionSelector('tags', sortByProp('label')), (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; @@ -25,6 +27,7 @@ function createMapStateToProps() { } const mapDispatchToProps = { + dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchNotifications: fetchNotifications, dispatchFetchIndexerProxies: fetchIndexerProxies, @@ -38,12 +41,14 @@ class MetadatasConnector extends Component { componentDidMount() { const { + dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchNotifications, dispatchFetchIndexerProxies, dispatchFetchApplications } = this.props; + dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchNotifications(); dispatchFetchIndexerProxies(); @@ -63,6 +68,7 @@ class MetadatasConnector extends Component { } MetadatasConnector.propTypes = { + dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchNotifications: PropTypes.func.isRequired, dispatchFetchIndexerProxies: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index caa013db3..d156f4ff3 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25' }, - { key: 'ddd MM/DD', value: 'Tue 03/25' }, - { key: 'ddd D/M', value: 'Tue 25/3' }, - { key: 'ddd DD/MM', value: 'Tue 25/03' } + { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' }, + { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' }, + { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' }, + { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' } ]; const shortDateFormatOptions = [ - { key: 'MMM D YYYY', value: 'Mar 25 2014' }, - { key: 'DD MMM YYYY', value: '25 Mar 2014' }, - { key: 'MM/D/YYYY', value: '03/25/2014' }, - { key: 'MM/DD/YYYY', value: '03/25/2014' }, - { key: 'DD/MM/YYYY', value: '25/03/2014' }, - { key: 'YYYY-MM-DD', value: '2014-03-25' } + { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' }, + { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' }, + { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' }, + { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' }, + { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' }, + { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' } ]; const longDateFormatOptions = [ diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index a80ee1e45..f5ef10a4d 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState'; import { set, updateServerSideCollection } from '../baseActions'; function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) { + const [baseSection] = section.split('.'); + return function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); @@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const { selectedFilterKey, - filters, - customFilters + filters } = sectionState; + const customFilters = getState().customFilters.items.filter((customFilter) => { + return customFilter.type === section || customFilter.type === baseSection; + }); + const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data + data, + traditional: true }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index ca26883fb..e35157dbd 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,8 +1,11 @@ +import $ from 'jquery'; +import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; +let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const testData = getProviderState(payload, getState, section); + const { + queryParams = {}, + ...otherPayload + } = payload; + + const testData = getProviderState({ ...otherPayload }, getState, section); + const params = { ...queryParams }; + + // If the user is re-testing the same provider without changes + // force it to be tested. + + if (_.isEqual(testData, lastTestData)) { + params.forceTest = true; + } + + lastTestData = testData; const ajaxOptions = { - url: `${url}/test`, + url: `${url}/test?${$.param(params, true)}`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { + lastTestData = null; + dispatch(set({ section, isTesting: false, diff --git a/frontend/src/Store/Actions/Settings/applications.js b/frontend/src/Store/Actions/Settings/applications.js index 3db520525..53a008b0c 100644 --- a/frontend/src/Store/Actions/Settings/applications.js +++ b/frontend/src/Store/Actions/Settings/applications.js @@ -1,4 +1,5 @@ import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; @@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -30,9 +32,10 @@ export const CANCEL_SAVE_APPLICATION = 'settings/applications/cancelSaveApplicat export const DELETE_APPLICATION = 'settings/applications/deleteApplication'; export const TEST_APPLICATION = 'settings/applications/testApplication'; export const CANCEL_TEST_APPLICATION = 'settings/applications/cancelTestApplication'; -export const TEST_ALL_APPLICATIONS = 'indexers/testAllApplications'; +export const TEST_ALL_APPLICATIONS = 'settings/applications/testAllApplications'; export const BULK_EDIT_APPLICATIONS = 'settings/applications/bulkEditApplications'; export const BULK_DELETE_APPLICATIONS = 'settings/applications/bulkDeleteApplications'; +export const SET_MANAGE_APPLICATIONS_SORT = 'settings/applications/setManageApplicationsSort'; // // Action Creators @@ -49,6 +52,7 @@ export const cancelTestApplication = createThunk(CANCEL_TEST_APPLICATION); export const testAllApplications = createThunk(TEST_ALL_APPLICATIONS); export const bulkEditApplications = createThunk(BULK_EDIT_APPLICATIONS); export const bulkDeleteApplications = createThunk(BULK_DELETE_APPLICATIONS); +export const setManageApplicationsSort = createAction(SET_MANAGE_APPLICATIONS_SORT); export const setApplicationValue = createAction(SET_APPLICATION_VALUE, (payload) => { return { @@ -88,7 +92,14 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {} + pendingChanges: {}, + sortKey: 'name', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + name: function(item) { + return item.name.toLowerCase(); + } + } }, // @@ -121,7 +132,10 @@ export default { return selectedSchema; }); - } + }, + + [SET_MANAGE_APPLICATIONS_SORT]: createSetClientSideCollectionSortReducer(section) + } }; diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 990b7008e..56784d5d0 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -1,4 +1,5 @@ import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; @@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -34,6 +36,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients'; export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients'; +export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort'; // // Action Creators @@ -50,6 +53,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT) export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS); export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS); +export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT); export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => { return { @@ -89,7 +93,14 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {} + pendingChanges: {}, + sortKey: 'name', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + name: function(item) { + return item.name.toLowerCase(); + } + } }, // @@ -147,7 +158,10 @@ export default { return selectedSchema; }); - } + }, + + [SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section) + } }; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 2e133f61e..c324fe227 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -1,5 +1,5 @@ import { createAction } from 'redux-actions'; -import { filterTypes, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -82,6 +82,12 @@ export const defaultState = { isSortable: false, isVisible: false }, + { + name: 'host', + label: () => translate('Host'), + isSortable: false, + isVisible: false + }, { name: 'elapsedTime', label: () => translate('ElapsedTime'), @@ -159,6 +165,27 @@ export const defaultState = { } ] } + ], + + filterBuilderProps: [ + { + name: 'eventType', + label: () => translate('EventType'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE + }, + { + name: 'indexerIds', + label: () => translate('Indexer'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.INDEXER + }, + { + name: 'successful', + label: () => translate('Successful'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.BOOL + } ] }; diff --git a/frontend/src/Store/Actions/indexerActions.js b/frontend/src/Store/Actions/indexerActions.js index ff0cfe5d4..e11051c2f 100644 --- a/frontend/src/Store/Actions/indexerActions.js +++ b/frontend/src/Store/Actions/indexerActions.js @@ -1,11 +1,15 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; +import { filterTypePredicates, sortDirections } from 'Helpers/Props'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; +import createSaveProviderHandler, { + createCancelSaveProviderHandler +} from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; -import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; +import createTestProviderHandler, { + createCancelTestProviderHandler +} from 'Store/Actions/Creators/createTestProviderHandler'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk, handleThunks } from 'Store/thunks'; @@ -16,6 +20,7 @@ import translate from 'Utilities/String/translate'; import createBulkEditItemHandler from './Creators/createBulkEditItemHandler'; import createBulkRemoveItemHandler from './Creators/createBulkRemoveItemHandler'; import createHandleActions from './Creators/createHandleActions'; +import createClearReducer from './Creators/Reducers/createClearReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; // @@ -69,15 +74,68 @@ export const filterPredicates = { item.fields.find((field) => field.name === 'vipExpiration')?.value ?? null; return dateFilterPredicate(vipExpiration, filterValue, type); + }, + + categories: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + + const { categories = [] } = item.capabilities || {}; + + const categoryList = categories + .filter((category) => category.id < 100000) + .reduce((acc, element) => { + acc.push(element.id); + + if (element.subCategories && element.subCategories.length > 0) { + element.subCategories.forEach((subCat) => { + acc.push(subCat.id); + }); + } + + return acc; + }, []); + + return predicate(categoryList, filterValue); } }; export const sortPredicates = { - vipExpiration: function(item) { - const vipExpiration = - item.fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; + status: function({ enable, redirect }) { + let result = 0; - return vipExpiration; + if (redirect) { + result++; + } + + if (enable) { + result += 2; + } + + return result; + }, + + vipExpiration: function({ fields = [] }) { + return fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; + }, + + minimumSeeders: function({ fields = [] }) { + return fields.find((field) => field.name === 'torrentBaseSettings.appMinimumSeeders')?.value ?? undefined; + }, + + seedRatio: function({ fields = [] }) { + return fields.find((field) => field.name === 'torrentBaseSettings.seedRatio')?.value ?? undefined; + }, + + seedTime: function({ fields = [] }) { + return fields.find((field) => field.name === 'torrentBaseSettings.seedTime')?.value ?? undefined; + }, + + packSeedTime: function({ fields = [] }) { + return fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')?.value ?? undefined; + }, + + preferMagnetUrl: function({ fields = [] }) { + return fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')?.value ?? undefined; } }; @@ -88,6 +146,7 @@ export const FETCH_INDEXERS = 'indexers/fetchIndexers'; export const FETCH_INDEXER_SCHEMA = 'indexers/fetchIndexerSchema'; export const SELECT_INDEXER_SCHEMA = 'indexers/selectIndexerSchema'; export const SET_INDEXER_SCHEMA_SORT = 'indexers/setIndexerSchemaSort'; +export const CLEAR_INDEXER_SCHEMA = 'indexers/clearIndexerSchema'; export const CLONE_INDEXER = 'indexers/cloneIndexer'; export const SET_INDEXER_VALUE = 'indexers/setIndexerValue'; export const SET_INDEXER_FIELD_VALUE = 'indexers/setIndexerFieldValue'; @@ -107,6 +166,7 @@ export const fetchIndexers = createThunk(FETCH_INDEXERS); export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); export const setIndexerSchemaSort = createAction(SET_INDEXER_SCHEMA_SORT); +export const clearIndexerSchema = createAction(CLEAR_INDEXER_SCHEMA); export const cloneIndexer = createAction(CLONE_INDEXER); export const saveIndexer = createThunk(SAVE_INDEXER); @@ -192,6 +252,8 @@ export const reducers = createHandleActions({ }); }, + [CLEAR_INDEXER_SCHEMA]: createClearReducer(schemaSection, defaultState), + [CLONE_INDEXER]: function(state, { payload }) { const id = payload.id; const newState = getSectionState(state, section); @@ -203,7 +265,13 @@ export const reducers = createHandleActions({ delete selectedSchema.name; selectedSchema.fields = selectedSchema.fields.map((field) => { - return { ...field }; + const newField = { ...field }; + + if (newField.privacy === 'apiKey' || newField.privacy === 'password') { + newField.value = ''; + } + + return newField; }); newState.selectedSchema = selectedSchema; diff --git a/frontend/src/Store/Actions/indexerIndexActions.js b/frontend/src/Store/Actions/indexerIndexActions.js index c55e46031..a002d9b41 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -37,12 +37,18 @@ export const defaultState = { isVisible: true, isModifiable: false }, + { + name: 'id', + columnLabel: () => translate('IndexerId'), + label: () => translate('Id'), + isSortable: true, + isVisible: false + }, { name: 'sortName', label: () => translate('IndexerName'), isSortable: true, - isVisible: true, - isModifiable: false + isVisible: true }, { name: 'protocol', @@ -110,6 +116,12 @@ export const defaultState = { isSortable: true, isVisible: false }, + { + name: 'preferMagnetUrl', + label: () => translate('PreferMagnetUrl'), + isSortable: true, + isVisible: false + }, { name: 'tags', label: () => translate('Tags'), @@ -180,6 +192,12 @@ export const defaultState = { type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.APP_PROFILE }, + { + name: 'categories', + label: () => translate('Categories'), + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.CATEGORY + }, { name: 'tags', label: () => translate('Tags'), diff --git a/frontend/src/Store/Actions/indexerStatsActions.js b/frontend/src/Store/Actions/indexerStatsActions.js index fe13c6db4..06c9586b5 100644 --- a/frontend/src/Store/Actions/indexerStatsActions.js +++ b/frontend/src/Store/Actions/indexerStatsActions.js @@ -74,8 +74,9 @@ export const defaultState = { valueType: filterBuilderValueTypes.TAG } ], - selectedFilterKey: 'all', - customFilters: [] + + selectedFilterKey: 'all' + }; export const persistState = [ diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 7ade2652d..fd2fe441b 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -369,8 +369,9 @@ export const actionHandlers = handleThunks({ promise.done((data) => { dispatch(batchActions([ - ...data.map((release) => { + ...data.map(({ guid }) => { return updateRelease({ + guid, isGrabbing: false, isGrabbed: true, grabError: null @@ -400,7 +401,16 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [CLEAR_RELEASES]: (state) => { - return Object.assign({}, state, defaultState); + const { + sortKey, + sortDirection, + customFilters, + selectedFilterKey, + columns, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); }, [UPDATE_RELEASE]: (state, { payload }) => { diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 92360b589..75d2595cf 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -110,7 +110,6 @@ export const defaultState = { { name: 'actions', columnLabel: () => translate('Actions'), - isSortable: true, isVisible: true, isModifiable: false } diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.js b/frontend/src/Store/Selectors/createDimensionsSelector.ts similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.js rename to frontend/src/Store/Selectors/createDimensionsSelector.ts index ce26b2e2c..b9602cb02 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.js +++ b/frontend/src/Store/Selectors/createDimensionsSelector.ts @@ -1,8 +1,9 @@ import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state) => state.app.dimensions, + (state: AppState) => state.app.dimensions, (dimensions) => { return dimensions; } diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts new file mode 100644 index 000000000..3a581587b --- /dev/null +++ b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; +import { DownloadClientAppState } from 'App/State/SettingsAppState'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import DownloadClient from 'typings/DownloadClient'; +import sortByProp from 'Utilities/Array/sortByProp'; + +export default function createEnabledDownloadClientsSelector( + protocol: DownloadProtocol +) { + return createSelector( + createSortedSectionSelector( + 'settings.downloadClients', + sortByProp('name') + ), + (downloadClients: DownloadClientAppState) => { + const { isFetching, isPopulated, error, items } = downloadClients; + + const clients = items.filter( + (item) => item.protocol === protocol && item.enable + ); + + return { isFetching, isPopulated, error, items: clients }; + } + ); +} diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.js b/frontend/src/Store/Selectors/createSortedSectionSelector.ts similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.js rename to frontend/src/Store/Selectors/createSortedSectionSelector.ts index 331d890c9..abee01f75 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.js +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.ts @@ -1,14 +1,18 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector(section, comparer) { +function createSortedSectionSelector( + section: string, + comparer: (a: T, b: T) => number +) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); + return { ...sectionState, - items: [...sectionState.items].sort(comparer) + items: [...sectionState.items].sort(comparer), }; } ); diff --git a/frontend/src/Styles/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index 79911686f..a7cbb6de0 100644 --- a/frontend/src/Styles/Themes/dark.js +++ b/frontend/src/Styles/Themes/dark.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#262626', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') }; diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index d93c5dd8c..4dec39164 100644 --- a/frontend/src/Styles/Themes/index.js +++ b/frontend/src/Styles/Themes/index.js @@ -2,7 +2,7 @@ import * as dark from './dark'; import * as light from './light'; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const auto = defaultDark ? { ...dark } : { ...light }; +const auto = defaultDark ? dark : light; export default { auto, diff --git a/frontend/src/Styles/Themes/light.js b/frontend/src/Styles/Themes/light.js index 8a6d123b2..f88070a0f 100644 --- a/frontend/src/Styles/Themes/light.js +++ b/frontend/src/Styles/Themes/light.js @@ -188,7 +188,7 @@ module.exports = { // Charts chartBackgroundColor: '#fff', - failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], - chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], - chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] + failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','), + chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','), + chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',') }; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index 3b0077c5a..def48f28e 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,7 +2,6 @@ module.exports = { // Families defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', - passwordFamily: 'text-security-disc', // Sizes extraSmallFontSize: '11px', diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index 5dfc7f940..39f7f1123 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -116,6 +116,7 @@ class BackupRow extends Component { @@ -138,7 +139,9 @@ class BackupRow extends Component { isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteBackup')} - message={translate('DeleteBackupMessageText', { name })} + message={translate('DeleteBackupMessageText', { + name + })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeletePress} onCancel={this.onConfirmDeleteModalClose} diff --git a/frontend/src/System/Backup/Backups.js b/frontend/src/System/Backup/Backups.js index 8f7a5b0a5..ede2f97f6 100644 --- a/frontend/src/System/Backup/Backups.js +++ b/frontend/src/System/Backup/Backups.js @@ -109,7 +109,7 @@ class Backups extends Component { { !isFetching && !!error && - {translate('UnableToLoadBackups')} + {translate('BackupsLoadError')} } diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.js b/frontend/src/System/Backup/RestoreBackupModalContent.js index 150c46ad6..9b5daa9f4 100644 --- a/frontend/src/System/Backup/RestoreBackupModalContent.js +++ b/frontend/src/System/Backup/RestoreBackupModalContent.js @@ -14,7 +14,7 @@ import styles from './RestoreBackupModalContent.css'; function getErrorMessage(error) { if (!error || !error.responseJSON || !error.responseJSON.message) { - return 'Error restoring backup'; + return translate('ErrorRestoringBackup'); } return error.responseJSON.message; @@ -146,7 +146,9 @@ class RestoreBackupModalContent extends Component { { - !!id && `Would you like to restore the backup '${name}'?` + !!id && translate('WouldYouLikeToRestoreBackup', { + name + }) } { @@ -203,7 +205,7 @@ class RestoreBackupModalContent extends Component {
- Note: Prowlarr will automatically restart and reload the UI during the restore process. + {translate('RestartReloadNote')}
- } - > - { - isFetching && !isPopulated && - - } - - { - !healthIssues && -
- {translate('HealthNoIssues')} -
- } - - { - healthIssues && - - - { - items.map((item) => { - const internalLink = getInternalLink(item.source); - const testLink = getTestLink(item.source, this.props); - - let kind = kinds.WARNING; - switch (item.type.toLowerCase()) { - case 'error': - kind = kinds.DANGER; - break; - default: - case 'warning': - kind = kinds.WARNING; - break; - case 'notice': - kind = kinds.INFO; - break; - } - - return ( - - - - - - {item.message} - - - - - { - internalLink - } - - { - !!testLink && - testLink - } - - - ); - }) - } - -
- } - - ); - } - -} - -Health.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - isTestingAllApplications: PropTypes.bool.isRequired, - isTestingAllDownloadClients: PropTypes.bool.isRequired, - isTestingAllIndexers: PropTypes.bool.isRequired, - dispatchTestAllApplications: PropTypes.func.isRequired, - dispatchTestAllDownloadClients: PropTypes.func.isRequired, - dispatchTestAllIndexers: PropTypes.func.isRequired -}; - -export default Health; diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx new file mode 100644 index 000000000..e0636961b --- /dev/null +++ b/frontend/src/System/Status/Health/Health.tsx @@ -0,0 +1,191 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import { icons, kinds } from 'Helpers/Props'; +import { testAllIndexers } from 'Store/Actions/indexerActions'; +import { + testAllApplications, + testAllDownloadClients, +} from 'Store/Actions/settingsActions'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import createHealthSelector from './createHealthSelector'; +import HealthItemLink from './HealthItemLink'; +import styles from './Health.css'; + +const columns: Column[] = [ + { + className: styles.status, + name: 'type', + label: '', + isVisible: true, + }, + { + name: 'message', + label: () => translate('Message'), + isVisible: true, + }, + { + name: 'actions', + label: () => translate('Actions'), + isVisible: true, + }, +]; + +function Health() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + createHealthSelector() + ); + const isTestingAllApplications = useSelector( + (state: AppState) => state.settings.applications.isTestingAll + ); + const isTestingAllDownloadClients = useSelector( + (state: AppState) => state.settings.downloadClients.isTestingAll + ); + const isTestingAllIndexers = useSelector( + (state: AppState) => state.indexers.isTestingAll + ); + + const healthIssues = !!items.length; + + const handleTestAllApplicationsPress = useCallback(() => { + dispatch(testAllApplications()); + }, [dispatch]); + + const handleTestAllDownloadClientsPress = useCallback(() => { + dispatch(testAllDownloadClients()); + }, [dispatch]); + + const handleTestAllIndexersPress = useCallback(() => { + dispatch(testAllIndexers()); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchHealth()); + }, [dispatch]); + + return ( +
+ {translate('Health')} + + {isFetching && isPopulated ? ( + + ) : null} + + } + > + {isFetching && !isPopulated ? : null} + + {isPopulated && !healthIssues ? ( +
+ {translate('NoIssuesWithYourConfiguration')} +
+ ) : null} + + {healthIssues ? ( + <> + + + {items.map((item) => { + const source = item.source; + + let kind = kinds.WARNING; + switch (item.type.toLowerCase()) { + case 'error': + kind = kinds.DANGER; + break; + default: + case 'warning': + kind = kinds.WARNING; + break; + case 'notice': + kind = kinds.INFO; + break; + } + + return ( + + + + + + {item.message} + + + + + + + {source === 'ApplicationStatusCheck' || + source === 'ApplicationLongTermStatusCheck' ? ( + + ) : null} + + {source === 'IndexerStatusCheck' || + source === 'IndexerLongTermStatusCheck' ? ( + + ) : null} + + {source === 'DownloadClientStatusCheck' ? ( + + ) : null} + + + ); + })} + +
+ + + + + + ) : null} +
+ ); +} + +export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js deleted file mode 100644 index 687e0ed87..000000000 --- a/frontend/src/System/Status/Health/HealthConnector.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { testAllIndexers } from 'Store/Actions/indexerActions'; -import { testAllApplications } from 'Store/Actions/Settings/applications'; -import { testAllDownloadClients } from 'Store/Actions/Settings/downloadClients'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; -import Health from './Health'; - -function createMapStateToProps() { - return createSelector( - createHealthCheckSelector(), - (state) => state.system.health, - (state) => state.settings.applications.isTestingAll, - (state) => state.settings.downloadClients.isTestingAll, - (state) => state.indexers.isTestingAll, - (items, health, isTestingAllApplications, isTestingAllDownloadClients, isTestingAllIndexers) => { - const { - isFetching, - isPopulated - } = health; - - return { - isFetching, - isPopulated, - items, - isTestingAllApplications, - isTestingAllDownloadClients, - isTestingAllIndexers - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchHealth: fetchHealth, - dispatchTestAllApplications: testAllApplications, - dispatchTestAllDownloadClients: testAllDownloadClients, - dispatchTestAllIndexers: testAllIndexers -}; - -class HealthConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchHealth(); - } - - // - // Render - - render() { - const { - dispatchFetchHealth, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -HealthConnector.propTypes = { - dispatchFetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthItemLink.tsx b/frontend/src/System/Status/Health/HealthItemLink.tsx new file mode 100644 index 000000000..b7a90c783 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +interface HealthItemLinkProps { + source: string; +} + +function HealthItemLink(props: HealthItemLinkProps) { + const { source } = props; + + switch (source) { + case 'ApplicationStatusCheck': + case 'ApplicationLongTermStatusCheck': + return ( + + ); + case 'DownloadClientStatusCheck': + return ( + + ); + case 'NotificationStatusCheck': + return ( + + ); + case 'IndexerProxyStatusCheck': + return ( + + ); + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + case 'IndexerLongTermStatusCheck': + return ( + + ); + case 'UpdateCheck': + return ( + + ); + default: + return null; + } +} + +export default HealthItemLink; diff --git a/frontend/src/System/Status/Health/HealthStatus.tsx b/frontend/src/System/Status/Health/HealthStatus.tsx new file mode 100644 index 000000000..b12fd3ebb --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatus.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import createHealthSelector from './createHealthSelector'; + +function HealthStatus() { + const dispatch = useDispatch(); + const { isConnected, isReconnecting } = useSelector( + (state: AppState) => state.app + ); + const { isPopulated, items } = useSelector(createHealthSelector()); + + const wasReconnecting = usePrevious(isReconnecting); + + const { count, errors, warnings } = useMemo(() => { + let errors = false; + let warnings = false; + + items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + count: items.length, + errors, + warnings, + }; + }, [items]); + + useEffect(() => { + if (!isPopulated) { + dispatch(fetchHealth()); + } + }, [isPopulated, dispatch]); + + useEffect(() => { + if (isConnected && wasReconnecting) { + dispatch(fetchHealth()); + } + }, [isConnected, wasReconnecting, dispatch]); + + return ( + + ); +} + +export default HealthStatus; diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js deleted file mode 100644 index e609dd712..000000000 --- a/frontend/src/System/Status/Health/HealthStatusConnector.js +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; -import { fetchHealth } from 'Store/Actions/systemActions'; -import createHealthCheckSelector from 'Store/Selectors/createHealthCheckSelector'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app, - createHealthCheckSelector(), - (state) => state.system.health, - (app, items, health) => { - const count = items.length; - let errors = false; - let warnings = false; - - items.forEach((item) => { - if (item.type === 'error') { - errors = true; - } - - if (item.type === 'warning') { - warnings = true; - } - }); - - return { - isConnected: app.isConnected, - isReconnecting: app.isReconnecting, - isPopulated: health.isPopulated, - count, - errors, - warnings - }; - } - ); -} - -const mapDispatchToProps = { - fetchHealth -}; - -class HealthStatusConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.fetchHealth(); - } - } - - componentDidUpdate(prevProps) { - if (this.props.isConnected && prevProps.isReconnecting) { - this.props.fetchHealth(); - } - } - - // - // Render - - render() { - return ( - - ); - } -} - -HealthStatusConnector.propTypes = { - isConnected: PropTypes.bool.isRequired, - isReconnecting: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - fetchHealth: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/Health/createHealthSelector.ts b/frontend/src/System/Status/Health/createHealthSelector.ts new file mode 100644 index 000000000..f38e3fe88 --- /dev/null +++ b/frontend/src/System/Status/Health/createHealthSelector.ts @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createHealthSelector() { + return createSelector( + (state: AppState) => state.system.health, + (health) => { + return health; + } + ); +} + +export default createHealthSelector; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js deleted file mode 100644 index dfb23a996..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ /dev/null @@ -1,58 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionList from 'Components/DescriptionList/DescriptionList'; -import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; -import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; -import FieldSet from 'Components/FieldSet'; -import Link from 'Components/Link/Link'; -import translate from 'Utilities/String/translate'; - -class MoreInfo extends Component { - - // - // Render - - render() { - return ( -
- - {translate('HomePage')} - - prowlarr.com - - - {translate('Wiki')} - - wiki.servarr.com/prowlarr - - - {translate('Reddit')} - - r/prowlarr - - - {translate('Discord')} - - prowlarr.com/discord - - - {translate('Source')} - - github.com/Prowlarr/Prowlarr - - - {translate('FeatureRequests')} - - github.com/Prowlarr/Prowlarr/issues - - - -
- ); - } -} - -MoreInfo.propTypes = { - -}; - -export default MoreInfo; diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx new file mode 100644 index 000000000..928449aed --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import FieldSet from 'Components/FieldSet'; +import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; + +function MoreInfo() { + return ( +
+ + + {translate('HomePage')} + + + prowlarr.com + + + {translate('Wiki')} + + + wiki.servarr.com/prowlarr + + + + + {translate('Reddit')} + + + r/prowlarr + + + + {translate('Discord')} + + + prowlarr.com/discord + + + + {translate('Source')} + + + + github.com/Prowlarr/Prowlarr + + + + + {translate('FeatureRequests')} + + + + github.com/Prowlarr/Prowlarr/issues + + + +
+ ); +} + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.js deleted file mode 100644 index 46e2d0951..000000000 --- a/frontend/src/System/Status/Status.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, { Component } from 'react'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import translate from 'Utilities/String/translate'; -import AboutConnector from './About/AboutConnector'; -import Donations from './Donations/Donations'; -import HealthConnector from './Health/HealthConnector'; -import MoreInfo from './MoreInfo/MoreInfo'; - -class Status extends Component { - - // - // Render - - render() { - return ( - - - - - - - - - ); - } - -} - -export default Status; diff --git a/frontend/src/System/Status/Status.tsx b/frontend/src/System/Status/Status.tsx new file mode 100644 index 000000000..6ae088160 --- /dev/null +++ b/frontend/src/System/Status/Status.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import translate from 'Utilities/String/translate'; +import About from './About/About'; +import Donations from './Donations/Donations'; +import Health from './Health/Health'; +import MoreInfo from './MoreInfo/MoreInfo'; + +function Status() { + return ( + + + + + + + + + ); +} + +export default Status; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css index 034804711..6e38929c9 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css @@ -10,15 +10,6 @@ width: 100%; } -.commandName { - display: inline-block; - min-width: 220px; -} - -.userAgent { - color: #b0b0b0; -} - .queued, .started, .ended { diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts index 3bc00b738..2c6010533 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts @@ -2,14 +2,12 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'commandName': string; 'duration': string; 'ended': string; 'queued': string; 'started': string; 'trigger': string; 'triggerContent': string; - 'userAgent': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js deleted file mode 100644 index 917bfa11a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js +++ /dev/null @@ -1,279 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './QueuedTaskRow.css'; - -function getStatusIconProps(status, message) { - const title = titleCase(status); - - switch (status) { - case 'queued': - return { - name: icons.PENDING, - title - }; - - case 'started': - return { - name: icons.REFRESH, - isSpinning: true, - title - }; - - case 'completed': - return { - name: icons.CHECK, - kind: kinds.SUCCESS, - title: message === 'Completed' ? title : `${title}: ${message}` - }; - - case 'failed': - return { - name: icons.FATAL, - kind: kinds.DANGER, - title: `${title}: ${message}` - }; - - default: - return { - name: icons.UNKNOWN, - title - }; - } -} - -function getFormattedDates(props) { - const { - queued, - started, - ended, - showRelativeDates, - shortDateFormat - } = props; - - if (showRelativeDates) { - return { - queuedAt: moment(queued).fromNow(), - startedAt: started ? moment(started).fromNow() : '-', - endedAt: ended ? moment(ended).fromNow() : '-' - }; - } - - return { - queuedAt: formatDate(queued, shortDateFormat), - startedAt: started ? formatDate(started, shortDateFormat) : '-', - endedAt: ended ? formatDate(ended, shortDateFormat) : '-' - }; -} - -class QueuedTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - ...getFormattedDates(props), - isCancelConfirmModalOpen: false - }; - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - queued, - started, - ended - } = this.props; - - if ( - queued !== prevProps.queued || - started !== prevProps.started || - ended !== prevProps.ended - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Control - - setUpdateTimer() { - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, 30000); - } - - // - // Listeners - - onCancelPress = () => { - this.setState({ - isCancelConfirmModalOpen: true - }); - }; - - onAbortCancel = () => { - this.setState({ - isCancelConfirmModalOpen: false - }); - }; - - // - // Render - - render() { - const { - trigger, - commandName, - queued, - started, - ended, - status, - duration, - message, - clientUserAgent, - longDateFormat, - timeFormat, - onCancelPress - } = this.props; - - const { - queuedAt, - startedAt, - endedAt, - isCancelConfirmModalOpen - } = this.state; - - let triggerIcon = icons.QUICK; - - if (trigger === 'manual') { - triggerIcon = icons.INTERACTIVE; - } else if (trigger === 'scheduled') { - triggerIcon = icons.SCHEDULED; - } - - return ( - - - - - - - - - - - - {commandName} - - { - clientUserAgent ? - - from: {clientUserAgent} - : - null - } - - - - {queuedAt} - - - - {startedAt} - - - - {endedAt} - - - - {formatTimeSpan(duration)} - - - - { - status === 'queued' && - - } - - - - - ); - } -} - -QueuedTaskRow.propTypes = { - trigger: PropTypes.string.isRequired, - commandName: PropTypes.string.isRequired, - queued: PropTypes.string.isRequired, - started: PropTypes.string, - ended: PropTypes.string, - status: PropTypes.string.isRequired, - duration: PropTypes.string, - message: PropTypes.string, - clientUserAgent: PropTypes.string, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onCancelPress: PropTypes.func.isRequired -}; - -export default QueuedTaskRow; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx new file mode 100644 index 000000000..4511bcbf4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx @@ -0,0 +1,238 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CommandBody } from 'Commands/Command'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import { cancelCommand } from 'Store/Actions/commandActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; +import styles from './QueuedTaskRow.css'; + +function getStatusIconProps(status: string, message: string | undefined) { + const title = titleCase(status); + + switch (status) { + case 'queued': + return { + name: icons.PENDING, + title, + }; + + case 'started': + return { + name: icons.REFRESH, + isSpinning: true, + title, + }; + + case 'completed': + return { + name: icons.CHECK, + kind: kinds.SUCCESS, + title: message === 'Completed' ? title : `${title}: ${message}`, + }; + + case 'failed': + return { + name: icons.FATAL, + kind: kinds.DANGER, + title: `${title}: ${message}`, + }; + + default: + return { + name: icons.UNKNOWN, + title, + }; + } +} + +function getFormattedDates( + queued: string, + started: string | undefined, + ended: string | undefined, + showRelativeDates: boolean, + shortDateFormat: string +) { + if (showRelativeDates) { + return { + queuedAt: moment(queued).fromNow(), + startedAt: started ? moment(started).fromNow() : '-', + endedAt: ended ? moment(ended).fromNow() : '-', + }; + } + + return { + queuedAt: formatDate(queued, shortDateFormat), + startedAt: started ? formatDate(started, shortDateFormat) : '-', + endedAt: ended ? formatDate(ended, shortDateFormat) : '-', + }; +} + +interface QueuedTimes { + queuedAt: string; + startedAt: string; + endedAt: string; +} + +export interface QueuedTaskRowProps { + id: number; + trigger: string; + commandName: string; + queued: string; + started?: string; + ended?: string; + status: string; + duration?: string; + message?: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRow(props: QueuedTaskRowProps) { + const { + id, + trigger, + commandName, + queued, + started, + ended, + status, + duration, + message, + body, + clientUserAgent, + } = props; + + const dispatch = useDispatch(); + const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } = + useSelector(createUISettingsSelector()); + + const updateTimeTimeoutId = useRef | null>( + null + ); + const [times, setTimes] = useState( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + + const [ + isCancelConfirmModalOpen, + openCancelConfirmModal, + closeCancelConfirmModal, + ] = useModalOpenState(false); + + const handleCancelPress = useCallback(() => { + dispatch(cancelCommand({ id })); + }, [id, dispatch]); + + useEffect(() => { + updateTimeTimeoutId.current = setTimeout(() => { + setTimes( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + }, 30000); + + return () => { + if (updateTimeTimeoutId.current) { + clearTimeout(updateTimeTimeoutId.current); + } + }; + }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]); + + const { queuedAt, startedAt, endedAt } = times; + + let triggerIcon = icons.QUICK; + + if (trigger === 'manual') { + triggerIcon = icons.INTERACTIVE; + } else if (trigger === 'scheduled') { + triggerIcon = icons.SCHEDULED; + } + + return ( + + + + + + + + + + + + + {queuedAt} + + + + {startedAt} + + + + {endedAt} + + + + {formatTimeSpan(duration)} + + + + {status === 'queued' && ( + + )} + + + + + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js deleted file mode 100644 index f55ab985a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js +++ /dev/null @@ -1,31 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { cancelCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import QueuedTaskRow from './QueuedTaskRow'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return { - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onCancelPress() { - dispatch(cancelCommand({ - id: props.id - })); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css new file mode 100644 index 000000000..41acb33f8 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css @@ -0,0 +1,8 @@ +.commandName { + display: inline-block; + min-width: 220px; +} + +.userAgent { + color: #b0b0b0; +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts new file mode 100644 index 000000000..fc9081492 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'commandName': string; + 'userAgent': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx new file mode 100644 index 000000000..601a57242 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { CommandBody } from 'Commands/Command'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import translate from 'Utilities/String/translate'; +import styles from './QueuedTaskRowNameCell.css'; + +export interface QueuedTaskRowNameCellProps { + commandName: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRowNameCell( + props: QueuedTaskRowNameCellProps +) { + const { commandName, clientUserAgent } = props; + + return ( + + {commandName} + + {clientUserAgent ? ( + + {translate('From')}: {clientUserAgent} + + ) : null} + + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js deleted file mode 100644 index dac38f1d4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRowConnector from './QueuedTaskRowConnector'; - -const columns = [ - { - name: 'trigger', - label: '', - isVisible: true - }, - { - name: 'commandName', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'queued', - label: () => translate('Queued'), - isVisible: true - }, - { - name: 'started', - label: () => translate('Started'), - isVisible: true - }, - { - name: 'ended', - label: () => translate('Ended'), - isVisible: true - }, - { - name: 'duration', - label: () => translate('Duration'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function QueuedTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( -
- { - isFetching && !isPopulated && - - } - - { - isPopulated && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- ); -} - -QueuedTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default QueuedTasks; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx new file mode 100644 index 000000000..e79deed7c --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchCommands } from 'Store/Actions/commandActions'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRow from './QueuedTaskRow'; + +const columns = [ + { + name: 'trigger', + label: '', + isVisible: true, + }, + { + name: 'commandName', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'queued', + label: () => translate('Queued'), + isVisible: true, + }, + { + name: 'started', + label: () => translate('Started'), + isVisible: true, + }, + { + name: 'ended', + label: () => translate('Ended'), + isVisible: true, + }, + { + name: 'duration', + label: () => translate('Duration'), + isVisible: true, + }, + { + name: 'actions', + isVisible: true, + }, +]; + +export default function QueuedTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.commands + ); + + useEffect(() => { + dispatch(fetchCommands()); + }, [dispatch]); + + return ( +
+ {isFetching && !isPopulated && } + + {isPopulated && ( + + + {items.map((item) => { + return ; + })} + +
+ )} +
+ ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js deleted file mode 100644 index 5fa4d9ead..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchCommands } from 'Store/Actions/commandActions'; -import QueuedTasks from './QueuedTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.commands, - (commands) => { - return commands; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchCommands: fetchCommands -}; - -class QueuedTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchCommands(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -QueuedTasksConnector.propTypes = { - dispatchFetchCommands: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector); diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js deleted file mode 100644 index acb8c8d36..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js +++ /dev/null @@ -1,203 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import styles from './ScheduledTaskRow.css'; - -function getFormattedDates(props) { - const { - lastExecution, - nextExecution, - interval, - showRelativeDates, - shortDateFormat - } = props; - - const isDisabled = interval === 0; - - if (showRelativeDates) { - return { - lastExecutionTime: moment(lastExecution).fromNow(), - nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow() - }; - } - - return { - lastExecutionTime: formatDate(lastExecution, shortDateFormat), - nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat) - }; -} - -class ScheduledTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = getFormattedDates(props); - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - lastExecution, - nextExecution - } = this.props; - - if ( - lastExecution !== prevProps.lastExecution || - nextExecution !== prevProps.nextExecution - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Listeners - - setUpdateTimer() { - const { interval } = this.props; - const timeout = interval < 60 ? 10000 : 60000; - - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, timeout); - } - - // - // Render - - render() { - const { - name, - interval, - lastExecution, - lastStartTime, - lastDuration, - nextExecution, - isQueued, - isExecuting, - longDateFormat, - timeFormat, - onExecutePress - } = this.props; - - const { - lastExecutionTime, - nextExecutionTime - } = this.state; - - const isDisabled = interval === 0; - const executeNow = !isDisabled && moment().isAfter(nextExecution); - const hasNextExecutionTime = !isDisabled && !executeNow; - const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); - const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); - - return ( - - {name} - - {isDisabled ? 'disabled' : duration} - - - - {lastExecutionTime} - - - { - !hasLastStartTime && - - - } - - { - hasLastStartTime && - - {formatTimeSpan(lastDuration)} - - } - - { - isDisabled && - - - } - - { - executeNow && isQueued && - queued - } - - { - executeNow && !isQueued && - now - } - - { - hasNextExecutionTime && - - {nextExecutionTime} - - } - - - - - - ); - } -} - -ScheduledTaskRow.propTypes = { - name: PropTypes.string.isRequired, - interval: PropTypes.number.isRequired, - lastExecution: PropTypes.string.isRequired, - lastStartTime: PropTypes.string.isRequired, - lastDuration: PropTypes.string.isRequired, - nextExecution: PropTypes.string.isRequired, - isQueued: PropTypes.bool.isRequired, - isExecuting: PropTypes.bool.isRequired, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onExecutePress: PropTypes.func.isRequired -}; - -export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx new file mode 100644 index 000000000..3a3cd02de --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx @@ -0,0 +1,170 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { isCommandExecuting } from 'Utilities/Command'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import styles from './ScheduledTaskRow.css'; + +interface ScheduledTaskRowProps { + id: number; + taskName: string; + name: string; + interval: number; + lastExecution: string; + lastStartTime: string; + lastDuration: string; + nextExecution: string; +} + +function ScheduledTaskRow(props: ScheduledTaskRowProps) { + const { + id, + taskName, + name, + interval, + lastExecution, + lastStartTime, + lastDuration, + nextExecution, + } = props; + + const dispatch = useDispatch(); + + const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } = + useSelector(createUISettingsSelector()); + const command = useSelector(createCommandSelector(taskName)); + + const [time, setTime] = useState(Date.now()); + + const isQueued = !!(command && command.status === 'queued'); + const isExecuting = isCommandExecuting(command); + const wasExecuting = usePrevious(isExecuting); + const isDisabled = interval === 0; + const executeNow = !isDisabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !isDisabled && !executeNow; + const hasLastStartTime = moment(lastStartTime).isAfter('2010-01-01'); + + const duration = useMemo(() => { + return moment + .duration(interval, 'minutes') + .humanize() + .replace(/an?(?=\s)/, '1'); + }, [interval]); + + const { lastExecutionTime, nextExecutionTime } = useMemo(() => { + const isDisabled = interval === 0; + + if (showRelativeDates && time) { + return { + lastExecutionTime: moment(lastExecution).fromNow(), + nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow(), + }; + } + + return { + lastExecutionTime: formatDate(lastExecution, shortDateFormat), + nextExecutionTime: isDisabled + ? '-' + : formatDate(nextExecution, shortDateFormat), + }; + }, [ + time, + interval, + lastExecution, + nextExecution, + showRelativeDates, + shortDateFormat, + ]); + + const handleExecutePress = useCallback(() => { + dispatch( + executeCommand({ + name: taskName, + }) + ); + }, [taskName, dispatch]); + + useEffect(() => { + if (!isExecuting && wasExecuting) { + setTimeout(() => { + dispatch(fetchTask({ id })); + }, 1000); + } + }, [id, isExecuting, wasExecuting, dispatch]); + + useEffect(() => { + const interval = setInterval(() => setTime(Date.now()), 1000); + return () => { + clearInterval(interval); + }; + }, [setTime]); + + return ( + + {name} + + {isDisabled ? 'disabled' : duration} + + + + {lastExecutionTime} + + + {hasLastStartTime ? ( + + {formatTimeSpan(lastDuration)} + + ) : ( + - + )} + + {isDisabled ? ( + - + ) : null} + + {executeNow && isQueued ? ( + queued + ) : null} + + {executeNow && !isQueued ? ( + now + ) : null} + + {hasNextExecutionTime ? ( + + {nextExecutionTime} + + ) : null} + + + + + + ); +} + +export default ScheduledTaskRow; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js deleted file mode 100644 index dae790d68..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js +++ /dev/null @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchTask } from 'Store/Actions/systemActions'; -import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import { findCommand, isCommandExecuting } from 'Utilities/Command'; -import ScheduledTaskRow from './ScheduledTaskRow'; - -function createMapStateToProps() { - return createSelector( - (state, { taskName }) => taskName, - createCommandsSelector(), - createUISettingsSelector(), - (taskName, commands, uiSettings) => { - const command = findCommand(commands, { name: taskName }); - - return { - isQueued: !!(command && command.state === 'queued'), - isExecuting: isCommandExecuting(command), - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - const taskName = props.taskName; - - return { - dispatchFetchTask() { - dispatch(fetchTask({ - id: props.id - })); - }, - - onExecutePress() { - dispatch(executeCommand({ - name: taskName - })); - } - }; -} - -class ScheduledTaskRowConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps) { - const { - isExecuting, - dispatchFetchTask - } = this.props; - - if (!isExecuting && prevProps.isExecuting) { - // Give the host a moment to update after the command completes - setTimeout(() => { - dispatchFetchTask(); - }, 1000); - } - } - - // - // Render - - render() { - const { - dispatchFetchTask, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -ScheduledTaskRowConnector.propTypes = { - id: PropTypes.number.isRequired, - isExecuting: PropTypes.bool.isRequired, - dispatchFetchTask: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector); diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js deleted file mode 100644 index bec151613..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import translate from 'Utilities/String/translate'; -import ScheduledTaskRowConnector from './ScheduledTaskRowConnector'; - -const columns = [ - { - name: 'name', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'interval', - label: () => translate('Interval'), - isVisible: true - }, - { - name: 'lastExecution', - label: () => translate('LastExecution'), - isVisible: true - }, - { - name: 'lastDuration', - label: () => translate('LastDuration'), - isVisible: true - }, - { - name: 'nextExecution', - label: () => translate('NextExecution'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function ScheduledTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( -
- { - isFetching && !isPopulated && - - } - - { - isPopulated && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- ); -} - -ScheduledTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx new file mode 100644 index 000000000..fcf5764bb --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Column from 'Components/Table/Column'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import translate from 'Utilities/String/translate'; +import ScheduledTaskRow from './ScheduledTaskRow'; + +const columns: Column[] = [ + { + name: 'name', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'interval', + label: () => translate('Interval'), + isVisible: true, + }, + { + name: 'lastExecution', + label: () => translate('LastExecution'), + isVisible: true, + }, + { + name: 'lastDuration', + label: () => translate('LastDuration'), + isVisible: true, + }, + { + name: 'nextExecution', + label: () => translate('NextExecution'), + isVisible: true, + }, + { + name: 'actions', + label: '', + isVisible: true, + }, +]; + +function ScheduledTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.system.tasks + ); + + useEffect(() => { + dispatch(fetchTasks()); + }, [dispatch]); + + return ( +
+ {isFetching && !isPopulated && } + + {isPopulated && ( + + + {items.map((item) => { + return ; + })} + +
+ )} +
+ ); +} + +export default ScheduledTasks; diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js deleted file mode 100644 index 8f418d3bb..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchTasks } from 'Store/Actions/systemActions'; -import ScheduledTasks from './ScheduledTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.tasks, - (tasks) => { - return tasks; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchTasks: fetchTasks -}; - -class ScheduledTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchTasks(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -ScheduledTasksConnector.propTypes = { - dispatchFetchTasks: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.tsx similarity index 63% rename from frontend/src/System/Tasks/Tasks.js rename to frontend/src/System/Tasks/Tasks.tsx index 032dbede8..26473d7ba 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.tsx @@ -2,15 +2,15 @@ import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import QueuedTasksConnector from './Queued/QueuedTasksConnector'; -import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; +import QueuedTasks from './Queued/QueuedTasks'; +import ScheduledTasks from './Scheduled/ScheduledTasks'; function Tasks() { return ( - - + + ); diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js deleted file mode 100644 index 9d6b9decc..000000000 --- a/frontend/src/System/Updates/UpdateChanges.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import styles from './UpdateChanges.css'; - -class UpdateChanges extends Component { - - // - // Render - - render() { - const { - title, - changes - } = this.props; - - if (changes.length === 0) { - return null; - } - - return ( -
-
{title}
-
    - { - changes.map((change, index) => { - const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => { - return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`; - }); - - return ( -
  • - -
  • - ); - }) - } -
-
- ); - } - -} - -UpdateChanges.propTypes = { - title: PropTypes.string.isRequired, - changes: PropTypes.arrayOf(PropTypes.string) -}; - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx new file mode 100644 index 000000000..460814cbe --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +interface UpdateChangesProps { + title: string; + changes: string[]; +} + +function UpdateChanges(props: UpdateChangesProps) { + const { title, changes } = props; + + if (changes.length === 0) { + return null; + } + + const uniqueChanges = [...new Set(changes)]; + + return ( +
+
{title}
+
    + {uniqueChanges.map((change, index) => { + const checkChange = change.replace( + /#\d{3,5}\b/g, + (match) => + `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring( + 1 + )})` + ); + + return ( +
  • + +
  • + ); + })} +
+
+ ); +} + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js deleted file mode 100644 index 40ab58c75..000000000 --- a/frontend/src/System/Updates/Updates.js +++ /dev/null @@ -1,252 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -class Updates extends Component { - - // - // Render - - render() { - const { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - isInstallingUpdate, - updateMechanism, - isDocker, - updateMechanismMessage, - shortDateFormat, - longDateFormat, - timeFormat, - onInstallLatestPress - } = this.props; - - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const externalUpdaterPrefix = 'Unable to update Prowlarr directly,'; - const externalUpdaterMessages = { - external: 'Prowlarr is configured to use an external update mechanism', - apt: 'use apt to install the update', - docker: 'update the docker container to receive the update' - }; - - return ( - - - { - !isPopulated && !hasError && - - } - - { - noUpdates && - - {translate('NoUpdatesAreAvailable')} - - } - - { - hasUpdateToInstall && -
- { - (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? - - Install Latest - : - - - - -
- {externalUpdaterPrefix} -
-
- } - - { - isFetching && - - } -
- } - - { - noUpdateToInstall && -
- - -
- {translate('TheLatestVersionIsAlreadyInstalled')} -
- - { - isFetching && - - } -
- } - - { - hasUpdates && -
- { - items.map((update) => { - const hasChanges = !!update.changes; - - return ( -
-
-
{update.version}
-
-
- {formatDate(update.releaseDate, shortDateFormat)} -
- - { - update.branch === 'master' ? - null: - - } - - { - update.version === currentVersion ? - : - null - } - - { - update.version !== currentVersion && update.installedOn ? - : - null - } -
- - { - !hasChanges && -
- {translate('MaintenanceRelease')} -
- } - - { - hasChanges && -
- - - -
- } -
- ); - }) - } -
- } - - { - !!updatesError && -
- Failed to fetch updates -
- } - - { - !!generalSettingsError && -
- Failed to update settings -
- } -
-
- ); - } - -} - -Updates.propTypes = { - currentVersion: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - updatesError: PropTypes.object, - generalSettingsError: PropTypes.object, - items: PropTypes.array.isRequired, - isInstallingUpdate: PropTypes.bool.isRequired, - isDocker: PropTypes.bool.isRequired, - updateMechanism: PropTypes.string, - updateMechanismMessage: PropTypes.string, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onInstallLatestPress: PropTypes.func.isRequired -}; - -export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx new file mode 100644 index 000000000..ea309a1cc --- /dev/null +++ b/frontend/src/System/Updates/Updates.tsx @@ -0,0 +1,303 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { UpdateMechanism } from 'typings/Settings/General'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; + +function createUpdatesSelector() { + return createSelector( + (state: AppState) => state.system.updates, + (state: AppState) => state.settings.general, + (updates, generalSettings) => { + const { error: updatesError, items } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + updateMechanism: generalSettings.item.updateMechanism, + }; + } + ); +} + +function Updates() { + const currentVersion = useSelector((state: AppState) => state.app.version); + const { packageUpdateMechanismMessage } = useSelector( + createSystemStatusSelector() + ); + const { shortDateFormat, longDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + const isInstallingUpdate = useSelector( + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) + ); + + const { + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + updateMechanism, + } = useSelector(createUpdatesSelector()); + + const dispatch = useDispatch(); + const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + + const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); + const externalUpdaterMessages: Partial> = { + external: translate('ExternalUpdater'), + apt: translate('AptUpdater'), + docker: translate('DockerUpdater'), + }; + + const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { + const majorVersion = parseInt( + currentVersion.match(VERSION_REGEX)?.[0] ?? '0' + ); + + const latestVersion = items[0]?.version; + const latestMajorVersion = parseInt( + latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' + ); + + return { + isMajorUpdate: latestMajorVersion > majorVersion, + hasUpdateToInstall: items.some( + (update) => update.installable && update.latest + ), + }; + }, [currentVersion, items]); + + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const handleInstallLatestPress = useCallback(() => { + if (isMajorUpdate) { + setIsMajorUpdateModalOpen(true); + } else { + dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); + } + }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); + + const handleInstallLatestMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + + dispatch( + executeCommand({ + name: commandNames.APPLICATION_UPDATE, + installMajorUpdate: true, + }) + ); + }, [setIsMajorUpdateModalOpen, dispatch]); + + const handleCancelMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + }, [setIsMajorUpdateModalOpen]); + + useEffect(() => { + dispatch(fetchUpdates()); + dispatch(fetchGeneralSettings()); + }, [dispatch]); + + return ( + + + {isPopulated || hasError ? null : } + + {noUpdates ? ( + {translate('NoUpdatesAreAvailable')} + ) : null} + + {hasUpdateToInstall ? ( +
+ {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( + + {translate('InstallLatest')} + + ) : ( + <> + + +
+ {externalUpdaterPrefix}{' '} + +
+ + )} + + {isFetching ? ( + + ) : null} +
+ ) : null} + + {noUpdateToInstall && ( +
+ +
{translate('OnLatestVersion')}
+ + {isFetching && ( + + )} +
+ )} + + {hasUpdates && ( +
+ {items.map((update) => { + return ( +
+
+
{update.version}
+
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
+ + {update.branch === 'master' ? null : ( + + )} + + {update.version === currentVersion ? ( + + ) : null} + + {update.version !== currentVersion && update.installedOn ? ( + + ) : null} +
+ + {update.changes ? ( +
+ + + +
+ ) : ( +
{translate('MaintenanceRelease')}
+ )} +
+ ); + })} +
+ )} + + {updatesError ? ( + + {translate('FailedToFetchUpdates')} + + ) : null} + + {generalSettingsError ? ( + + {translate('FailedToFetchSettings')} + + ) : null} + + +
{translate('InstallMajorVersionUpdateMessage')}
+
+ +
+ + } + confirmLabel={translate('Install')} + onConfirm={handleInstallLatestMajorVersionPress} + onCancel={handleCancelMajorVersionPress} + /> +
+
+ ); +} + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js deleted file mode 100644 index 38873a990..000000000 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import Updates from './Updates'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - createSystemStatusSelector(), - (state) => state.system.updates, - (state) => state.settings.general, - createUISettingsSelector(), - createSystemStatusSelector(), - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), - ( - currentVersion, - status, - updates, - generalSettings, - uiSettings, - systemStatus, - isInstallingUpdate - ) => { - const { - error: updatesError, - items - } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - isInstallingUpdate, - isDocker: systemStatus.isDocker, - updateMechanism: generalSettings.item.updateMechanism, - updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchUpdates: fetchUpdates, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchExecuteCommand: executeCommand -}; - -class UpdatesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - this.props.dispatchFetchGeneralSettings(); - } - - // - // Listeners - - onInstallLatestPress = () => { - this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -UpdatesConnector.propTypes = { - dispatchFetchUpdates: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchExecuteCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js deleted file mode 100644 index 1956d3bac..000000000 --- a/frontend/src/Utilities/Array/sortByName.js +++ /dev/null @@ -1,5 +0,0 @@ -function sortByName(a, b) { - return a.name.localeCompare(b.name); -} - -export default sortByName; diff --git a/frontend/src/Utilities/Array/sortByProp.ts b/frontend/src/Utilities/Array/sortByProp.ts new file mode 100644 index 000000000..8fbde08c9 --- /dev/null +++ b/frontend/src/Utilities/Array/sortByProp.ts @@ -0,0 +1,13 @@ +import { StringKey } from 'typings/Helpers/KeysMatching'; + +export function sortByProp< + // eslint-disable-next-line no-use-before-define + T extends Record, + K extends StringKey +>(sortKey: K) { + return (a: T, b: T) => { + return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true }); + }; +} + +export default sortByProp; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.ts similarity index 61% rename from frontend/src/Utilities/Number/formatBytes.js rename to frontend/src/Utilities/Number/formatBytes.ts index 2fb3eebe6..a0ae8a985 100644 --- a/frontend/src/Utilities/Number/formatBytes.js +++ b/frontend/src/Utilities/Number/formatBytes.ts @@ -1,16 +1,16 @@ import { filesize } from 'filesize'; -function formatBytes(input) { +function formatBytes(input: string | number) { const size = Number(input); if (isNaN(size)) { return ''; } - return filesize(size, { + return `${filesize(size, { base: 2, - round: 1 - }); + round: 1, + })}`; } export default formatBytes; diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 1faa70ecc..72d3adf40 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise { translations = data.Strings; resolve(true); - } catch (error) { + } catch { resolve(false); } }); diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js deleted file mode 100644 index b687f2682..000000000 --- a/frontend/src/Utilities/getPathWithUrlBase.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getPathWithUrlBase(path) { - return `${window.Prowlarr.urlBase}${path}`; -} diff --git a/frontend/src/Utilities/getPathWithUrlBase.ts b/frontend/src/Utilities/getPathWithUrlBase.ts new file mode 100644 index 000000000..948456728 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.ts @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path: string) { + return `${window.Prowlarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js index dae5150b7..1b380851d 100644 --- a/frontend/src/Utilities/getUniqueElementId.js +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -1,7 +1,9 @@ let i = 0; -// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - +/** + * @deprecated Use React's useId() instead + * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + */ export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 6d08e7746..5efc89448 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,13 +3,16 @@ - - - + + + + + + diff --git a/frontend/src/index.js b/frontend/src/index.js deleted file mode 100644 index acdfc6517..000000000 --- a/frontend/src/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import { createBrowserHistory } from 'history'; -import React from 'react'; -import { render } from 'react-dom'; -import { fetchTranslations } from 'Utilities/String/translate'; - -import './preload'; -import './polyfills'; -import 'Styles/globals.css'; -import './index.css'; - -const history = createBrowserHistory(); -const hasTranslationsError = !await fetchTranslations(); - -const { default: createAppStore } = await import('Store/createAppStore'); -const { default: App } = await import('./App/App'); - -const store = createAppStore(history); - -render( - , - document.getElementById('root') -); diff --git a/frontend/src/login.html b/frontend/src/login.html index dcfb23140..d8af7f73f 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,15 +3,18 @@ - - + + + + + - + body { - background-color: #f5f7fa; - color: #656565; + background-color: var(--pageBackground); + color: var(--textColor); font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } @@ -85,14 +88,14 @@ padding: 10px; border-top-left-radius: 4px; border-top-right-radius: 4px; - background-color: #464b51; + background-color: var(--themeDarkColor); } .panel-body { padding: 20px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; - background-color: #fff; + background-color: var(--panelBackground); } .sign-in { @@ -109,16 +112,18 @@ padding: 6px 16px; width: 100%; height: 35px; - border: 1px solid #dde6e9; + background-color: var(--inputBackgroundColor); + border: 1px solid var(--inputBorderColor); border-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); + color: var(--textColor); } .form-input:focus { outline: 0; - border-color: #66afe9; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), - 0 0 8px rgba(102, 175, 233, 0.6); + border-color: var(--inputFocusBorderColor); + box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), + 0 0 8px var(--inputFocusBoxShadowColor); } .button { @@ -127,10 +132,10 @@ padding: 10px 0; width: 100%; border: 1px solid; - border-color: #5899eb; + border-color: var(--primaryBorderColor); border-radius: 4px; - background-color: #5d9cec; - color: #fff; + background-color: var(--primaryBackgroundColor); + color: var(--white); vertical-align: middle; text-align: center; white-space: nowrap; @@ -138,9 +143,9 @@ } .button:hover { - border-color: #3483e7; - background-color: #4b91ea; - color: #fff; + border-color: var(--primaryHoverBorderColor); + background-color: var(--primaryHoverBackgroundColor); + color: var(--white); text-decoration: none; } @@ -162,24 +167,24 @@ .forgot-password { margin-left: auto; - color: #909fa7; + color: var(--forgotPasswordColor); text-decoration: none; font-size: 13px; } .forgot-password:focus, .forgot-password:hover { - color: #748690; + color: var(--forgotPasswordAltColor); text-decoration: underline; } .forgot-password:visited { - color: #748690; + color: var(--forgotPasswordAltColor); } .login-failed { margin-top: 20px; - color: #f05050; + color: var(--failedColor); font-size: 14px; } @@ -288,5 +293,59 @@ loginFailedDiv.classList.remove("hidden"); } + + var light = { + white: '#fff', + pageBackground: '#f5f7fa', + textColor: '#515253', + themeDarkColor: '#464b51', + panelBackground: '#fff', + inputBackgroundColor: '#fff', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + primaryBackgroundColor: '#5d9cec', + primaryBorderColor: '#5899eb', + primaryHoverBackgroundColor: '#4b91ea', + primaryHoverBorderColor: '#3483e7', + failedColor: '#f05050', + forgotPasswordColor: '#909fa7', + forgotPasswordAltColor: '#748690' + }; + + var dark = { + white: '#fff', + pageBackground: '#202020', + textColor: '#ccc', + themeDarkColor: '#494949', + panelBackground: '#111', + inputBackgroundColor: '#333', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + primaryBackgroundColor: '#5d9cec', + primaryBorderColor: '#5899eb', + primaryHoverBackgroundColor: '#4b91ea', + primaryHoverBorderColor: '#3483e7', + failedColor: '#f05050', + forgotPasswordColor: '#737d83', + forgotPasswordAltColor: '#546067' + }; + + var theme = "_THEME_"; + var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ? + dark : + light; + + Object.entries(finalTheme).forEach(([key, value]) => { + document.documentElement.style.setProperty( + `--${key}`, + value + ); + }); + diff --git a/frontend/src/typings/Health.ts b/frontend/src/typings/Health.ts new file mode 100644 index 000000000..66f385bbb --- /dev/null +++ b/frontend/src/typings/Health.ts @@ -0,0 +1,8 @@ +interface Health { + source: string; + type: string; + message: string; + wikiUrl: string; +} + +export default Health; diff --git a/frontend/src/typings/Helpers/KeysMatching.ts b/frontend/src/typings/Helpers/KeysMatching.ts new file mode 100644 index 000000000..0e20206ef --- /dev/null +++ b/frontend/src/typings/Helpers/KeysMatching.ts @@ -0,0 +1,7 @@ +type KeysMatching = { + [K in keyof T]-?: T[K] extends V ? K : never; +}[keyof T]; + +export type StringKey = KeysMatching; + +export default KeysMatching; diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts index ff13c676a..3e50355dc 100644 --- a/frontend/src/typings/History.ts +++ b/frontend/src/typings/History.ts @@ -1,5 +1,12 @@ import ModelBase from 'App/ModelBase'; +export type HistoryQueryType = + | 'search' + | 'tvsearch' + | 'movie' + | 'book' + | 'music'; + export interface HistoryData { source: string; host: string; @@ -7,7 +14,7 @@ export interface HistoryData { offset: number; elapsedTime: number; query: string; - queryType: string; + queryType: HistoryQueryType; } interface History extends ModelBase { diff --git a/frontend/src/typings/IndexerStats.ts b/frontend/src/typings/IndexerStats.ts index 74fa1862e..ddbcebaec 100644 --- a/frontend/src/typings/IndexerStats.ts +++ b/frontend/src/typings/IndexerStats.ts @@ -2,6 +2,7 @@ export interface IndexerStatsIndexer { indexerId: number; indexerName: string; averageResponseTime: number; + averageGrabResponseTime: number; numberOfQueries: number; numberOfGrabs: number; numberOfRssQueries: number; diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts new file mode 100644 index 000000000..c867bed74 --- /dev/null +++ b/frontend/src/typings/Settings/General.ts @@ -0,0 +1,45 @@ +export type UpdateMechanism = + | 'builtIn' + | 'script' + | 'external' + | 'apt' + | 'docker'; + +export default interface General { + bindAddress: string; + port: number; + sslPort: number; + enableSsl: boolean; + launchBrowser: boolean; + authenticationMethod: string; + authenticationRequired: string; + analyticsEnabled: boolean; + username: string; + password: string; + passwordConfirmation: string; + logLevel: string; + consoleLogLevel: string; + branch: string; + apiKey: string; + sslCertPath: string; + sslCertPassword: string; + urlBase: string; + instanceName: string; + applicationUrl: string; + updateAutomatically: boolean; + updateMechanism: UpdateMechanism; + updateScriptPath: string; + proxyEnabled: boolean; + proxyType: string; + proxyHostname: string; + proxyPort: number; + proxyUsername: string; + proxyPassword: string; + proxyBypassFilter: string; + proxyBypassLocalAddresses: boolean; + certificateValidation: string; + backupFolder: string; + backupInterval: number; + backupRetention: number; + id: number; +} diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts similarity index 59% rename from frontend/src/typings/UiSettings.ts rename to frontend/src/typings/Settings/UiSettings.ts index 79cb0f333..656c4518b 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -1,4 +1,5 @@ -export interface UiSettings { +export default interface UiSettings { + theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; longDateFormat: string; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index e72be2c5c..d5eab3ca3 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -4,6 +4,8 @@ interface SystemStatus { authentication: string; branch: string; buildTime: string; + databaseVersion: string; + databaseType: string; instanceName: string; isAdmin: boolean; isDebug: boolean; @@ -18,7 +20,10 @@ interface SystemStatus { mode: string; osName: string; osVersion: string; + packageAuthor: string; packageUpdateMechanism: string; + packageUpdateMechanismMessage: string; + packageVersion: string; runtimeName: string; runtimeVersion: string; sqliteVersion: string; diff --git a/frontend/src/typings/Table.ts b/frontend/src/typings/Table.ts new file mode 100644 index 000000000..4f99e2045 --- /dev/null +++ b/frontend/src/typings/Table.ts @@ -0,0 +1,6 @@ +import Column from 'Components/Table/Column'; + +export interface TableOptionsChangePayload { + pageSize?: number; + columns: Column[]; +} diff --git a/frontend/src/typings/Task.ts b/frontend/src/typings/Task.ts new file mode 100644 index 000000000..57895d73e --- /dev/null +++ b/frontend/src/typings/Task.ts @@ -0,0 +1,13 @@ +import ModelBase from 'App/ModelBase'; + +interface Task extends ModelBase { + name: string; + taskName: string; + interval: number; + lastExecution: string; + lastStartTime: string; + nextExecution: string; + lastDuration: string; +} + +export default Task; diff --git a/frontend/src/typings/Update.ts b/frontend/src/typings/Update.ts new file mode 100644 index 000000000..448b1728d --- /dev/null +++ b/frontend/src/typings/Update.ts @@ -0,0 +1,20 @@ +export interface Changes { + new: string[]; + fixed: string[]; +} + +interface Update { + version: string; + branch: string; + releaseDate: string; + fileName: string; + url: string; + installed: boolean; + installedOn: string; + installable: boolean; + latest: boolean; + changes: Changes | null; + hash: string; +} + +export default Update; diff --git a/package.json b/package.json index 0f15e6f5f..25960d641 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/", "lint-fix": "yarn lint --fix", "stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc", - "stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc", + "stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc", "check-modules": "are-you-es5 check . -r" }, "repository": "https://github.com/Prowlarr/Prowlarr", @@ -23,35 +23,34 @@ "defaults" ], "dependencies": { - "@fortawesome/fontawesome-free": "6.4.0", - "@fortawesome/fontawesome-svg-core": "6.4.0", - "@fortawesome/free-regular-svg-icons": "6.4.0", - "@fortawesome/free-solid-svg-icons": "6.4.0", - "@fortawesome/react-fontawesome": "0.2.0", + "@fortawesome/fontawesome-free": "6.7.1", + "@fortawesome/fontawesome-svg-core": "6.7.1", + "@fortawesome/free-regular-svg-icons": "6.7.1", + "@fortawesome/free-solid-svg-icons": "6.7.1", + "@fortawesome/react-fontawesome": "0.2.2", "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.25", - "@sentry/browser": "7.51.2", - "@sentry/integrations": "7.51.2", - "@types/node": "18.15.11", - "@types/react": "18.2.6", - "@types/react-dom": "18.2.4", - "chart.js": "4.3.0", - "classnames": "2.3.2", - "clipboard": "2.0.11", + "@sentry/browser": "7.119.1", + "@sentry/integrations": "7.119.1", + "@types/node": "20.16.11", + "@types/react": "18.2.79", + "@types/react-dom": "18.2.25", + "chart.js": "4.4.4", + "classnames": "2.5.1", "connected-react-router": "6.9.3", + "copy-to-clipboard": "3.3.3", "element-class": "0.2.2", - "filesize": "10.0.7", + "filesize": "10.1.6", "history": "4.10.1", - "https-browserify": "1.0.0", "jdu": "1.0.0", - "jquery": "3.7.0", + "jquery": "3.7.1", "lodash": "4.17.21", "mobile-detect": "1.4.5", - "moment": "2.29.4", + "moment": "2.30.1", "mousetrap": "1.6.5", "normalize.css": "8.0.1", "prop-types": "15.8.1", - "qs": "6.11.1", + "qs": "6.13.0", "react": "17.0.2", "react-addons-shallow-compare": "15.6.3", "react-async-script": "1.2.0", @@ -65,7 +64,6 @@ "react-dom": "17.0.2", "react-focus-lock": "2.9.4", "react-google-recaptcha": "2.1.0", - "react-lazyload": "3.2.0", "react-measure": "1.4.7", "react-popper": "1.3.7", "react-redux": "7.2.4", @@ -75,73 +73,72 @@ "react-text-truncate": "0.19.0", "react-use-measure": "2.1.1", "react-virtualized": "9.21.1", - "react-window": "1.8.8", + "react-window": "1.8.10", "redux": "4.2.1", "redux-actions": "2.6.5", "redux-batched-actions": "0.5.0", "redux-localstorage": "0.4.1", "redux-thunk": "2.4.2", - "reselect": "4.1.7", + "reselect": "4.1.8", "stacktrace-js": "2.0.2", - "typescript": "5.0.4" + "typescript": "5.7.2" }, "devDependencies": { - "@babel/core": "7.22.11", - "@babel/eslint-parser": "7.22.11", - "@babel/plugin-proposal-export-default-from": "7.22.5", + "@babel/core": "7.26.0", + "@babel/eslint-parser": "7.25.9", + "@babel/plugin-proposal-export-default-from": "7.25.9", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.22.14", - "@babel/preset-react": "7.22.5", - "@babel/preset-typescript": "7.22.11", - "@types/lodash": "4.14.194", + "@babel/preset-env": "7.26.0", + "@babel/preset-react": "7.26.3", + "@babel/preset-typescript": "7.26.0", + "@types/lodash": "4.14.195", + "@types/react-document-title": "2.0.10", "@types/react-router-dom": "5.3.3", - "@types/react-text-truncate": "0.14.1", - "@types/react-window": "1.8.5", - "@types/webpack-livereload-plugin": "2.3.3", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@types/react-text-truncate": "0.19.0", + "@types/react-window": "1.8.8", + "@types/webpack-livereload-plugin": "2.3.6", + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", "are-you-es5": "2.1.2", - "autoprefixer": "10.4.14", - "babel-loader": "9.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.33.0", + "core-js": "3.39.0", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", - "eslint": "8.45.0", - "eslint-config-prettier": "8.8.0", + "eslint": "8.57.1", + "eslint-config-prettier": "8.10.0", "eslint-plugin-filenames": "1.3.2", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.31.0", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.32.2", - "eslint-plugin-react-hooks": "4.6.0", - "eslint-plugin-simple-import-sort": "10.0.0", + "eslint-plugin-react": "7.37.1", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-simple-import-sort": "12.1.1", "file-loader": "6.2.0", "filemanager-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0", - "html-webpack-plugin": "5.5.1", + "html-webpack-plugin": "5.6.0", "loader-utils": "^3.2.1", - "mini-css-extract-plugin": "2.7.5", - "postcss": "8.4.31", + "mini-css-extract-plugin": "2.9.1", + "postcss": "8.4.47", "postcss-color-function": "4.1.0", "postcss-loader": "7.3.0", "postcss-mixins": "9.0.4", - "postcss-nested": "6.0.1", + "postcss-nested": "6.2.0", "postcss-simple-vars": "7.0.1", "postcss-url": "10.1.3", "prettier": "2.8.8", "require-nocache": "1.0.0", - "rimraf": "4.4.1", - "run-sequence": "2.2.1", - "streamqueue": "1.1.2", + "rimraf": "6.0.1", "style-loader": "3.3.2", "stylelint": "15.6.1", - "stylelint-order": "6.0.3", - "terser-webpack-plugin": "5.3.9", - "ts-loader": "9.4.2", + "stylelint-order": "6.0.4", + "terser-webpack-plugin": "5.3.10", + "ts-loader": "9.5.1", "typescript-plugin-css-modules": "5.0.1", "url-loader": "4.1.1", - "webpack": "5.89.0", + "webpack": "5.95.0", "webpack-cli": "5.1.4", "webpack-livereload-plugin": "3.0.2" } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a926a45d4..ce3672c38 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -99,20 +99,52 @@ $(MSBuildProjectName.Replace('Prowlarr','NzbDrone')) - - - false + + + + + + + + + + + + + + + + + + true + + + + true + + true - - + + + + + + + + false + + @@ -144,22 +176,52 @@ + + + + + x64 + + + + + x86 + + + + + arm64 + + + + + arm + + + + + + + + + <_UsingDefaultRuntimeIdentifier>true - win-x64 + win-$(Architecture) <_UsingDefaultRuntimeIdentifier>true - linux-x64 + linux-$(Architecture) <_UsingDefaultRuntimeIdentifier>true - osx-x64 + osx-$(Architecture) diff --git a/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj b/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj index bb0b5fcc4..78c8b7d0f 100644 --- a/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj +++ b/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj @@ -4,7 +4,7 @@ - + diff --git a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index dd32e187d..e9d4aa3b0 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using FluentAssertions; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Test.Common; @@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test Mocker.GetMock() .Setup(v => v.WriteAllText(configFile, It.IsAny())) .Callback((p, t) => _configFileContents = t); + + Mocker.GetMock>() + .Setup(v => v.Value) + .Returns(new AuthOptions()); + + Mocker.GetMock>() + .Setup(v => v.Value) + .Returns(new AppOptions()); + + Mocker.GetMock>() + .Setup(v => v.Value) + .Returns(new ServerOptions()); + + Mocker.GetMock>() + .Setup(v => v.Value) + .Returns(new LogOptions()); + + Mocker.GetMock>() + .Setup(v => v.Value) + .Returns(new UpdateOptions()); } [Test] diff --git a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs index ff5d7383e..0f7ad3004 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs @@ -21,9 +21,28 @@ namespace NzbDrone.Common.Test.ExtensionTests [TestCase("1.2.3.4")] [TestCase("172.55.0.1")] [TestCase("192.55.0.1")] + [TestCase("100.64.0.1")] + [TestCase("100.127.255.254")] public void should_return_false_for_public_ip_address(string ipAddress) { IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse(); } + + [TestCase("100.64.0.1")] + [TestCase("100.127.255.254")] + [TestCase("100.100.100.100")] + public void should_return_true_for_cgnat_ip_address(string ipAddress) + { + IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeTrue(); + } + + [TestCase("1.2.3.4")] + [TestCase("192.168.5.1")] + [TestCase("100.63.255.255")] + [TestCase("100.128.0.0")] + public void should_return_false_for_non_cgnat_ip_address(string ipAddress) + { + IPAddress.Parse(ipAddress).IsCgnatIpAddress().Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs similarity index 91% rename from src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs rename to src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs index 76e28f3f7..c51ab7ad4 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs @@ -1,11 +1,11 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Test.ExtensionTests { [TestFixture] - public class Int64ExtensionFixture + public class NumberExtensionFixture { [TestCase(0, "0 B")] [TestCase(1000, "1,000.0 B")] diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index ea42239e8..9e2b31d87 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"https://beyond-hd.me/torrent/download/the-next-365-days-2022-2160p-nf-web-dl-dual-ddp-51-dovi-hdr-hevc-apex.225146.2b51db35e1912ffc138825a12b9933d2")] [TestCase(@"https://anthelion.me/api.php?api_key=2b51db35e1910123321025a12b9933d2&o=json&t=movie&q=&tmdb=&imdb=&cat=&limit=100&offset=0")] [TestCase(@"https://avistaz.to/api/v1/jackett/auth: username=mySecret&password=mySecret&pid=mySecret")] + [TestCase(@"https://www.sharewood.tv/api/2b51db35e1910123321025a12b9933d2/last-torrents")] + [TestCase(@"https://example.org/rss/torrents?rsskey=2b51db35e1910123321025a12b9933d2&search=")] // Indexer and Download Client Responses @@ -119,6 +121,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")] [TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")] [TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")] + + // Discord + [TestCase(@"https://discord.com/api/webhooks/mySecret")] + [TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")] public void should_clean_message(string message) { var cleansedMessage = CleanseLogMessage.Cleanse(message); diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs index 7392c3b85..b60fe2e54 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs @@ -4,6 +4,7 @@ using System.Linq; using FluentAssertions; using NLog; using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Test.Common; @@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests [SetUp] public void Setup() { - _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111"); + _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock().Object); } private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index a33a53c01..010bc3a02 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -3,6 +3,7 @@ using System.IO; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Test.Common; @@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test [TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")] [TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")] [TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")] - [TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")] + [TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")] [TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")] public void Clean_Path_Windows(string dirty, string clean) { @@ -132,11 +133,16 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\test\", @"C:\Test\mydir")] [TestCase(@"C:\test", @"C:\Test\mydir\")] - public void path_should_be_parent_on_windows_only(string parentPath, string childPath) + public void windows_path_should_be_parent(string parentPath, string childPath) { - var expectedResult = OsInfo.IsWindows; + parentPath.IsParentPath(childPath).Should().Be(true); + } - parentPath.IsParentPath(childPath).Should().Be(expectedResult); + [TestCase("/test", "/test/mydir/")] + [TestCase("/test/", "/test/mydir")] + public void posix_path_should_be_parent(string parentPath, string childPath) + { + parentPath.IsParentPath(childPath).Should().Be(true); } [TestCase(@"C:\Test\mydir", @"C:\Test")] @@ -144,20 +150,57 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] - public void path_should_return_parent_windows(string path, string parentPath) + public void windows_path_should_return_parent(string path, string parentPath) { - WindowsOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] - public void path_should_return_parent_mono(string path, string parentPath) + [TestCase(@"/test/tv", "/test")] + public void unix_path_should_return_parent(string path, string parentPath) { - PosixOnly(); path.GetParentPath().Should().Be(parentPath); } + [TestCase(@"C:\Test\mydir", "Test")] + [TestCase(@"C:\Test\", @"C:\")] + [TestCase(@"C:\Test", @"C:\")] + [TestCase(@"C:\", null)] + [TestCase(@"\\server\share", null)] + [TestCase(@"\\server\share\test", @"\\server\share")] + public void path_should_return_parent_name_windows(string path, string parentPath) + { + path.GetParentName().Should().Be(parentPath); + } + + [TestCase(@"/", null)] + [TestCase(@"/test", "/")] + [TestCase(@"/test/tv", "test")] + public void path_should_return_parent_name_mono(string path, string parentPath) + { + path.GetParentName().Should().Be(parentPath); + } + + [TestCase(@"C:\Test\mydir", "mydir")] + [TestCase(@"C:\Test\", "Test")] + [TestCase(@"C:\Test", "Test")] + [TestCase(@"C:\", "C:\\")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\test", "test")] + public void path_should_return_directory_name_windows(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + + [TestCase(@"/", "/")] + [TestCase(@"/test", "test")] + [TestCase(@"/test/tv", "tv")] + public void path_should_return_directory_name_mono(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + [Test] public void path_should_return_parent_for_oversized_path() { @@ -165,7 +208,7 @@ namespace NzbDrone.Common.Test // This test will fail on Windows if long path support is not enabled: https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/ // It will also fail if the app isn't configured to use long path (such as resharper): https://blogs.msdn.microsoft.com/jeremykuhne/2016/07/30/net-4-6-2-and-long-paths-on-windows-10/ - var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic(); + var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic(); var parentPath = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing".AsOsAgnostic(); path.GetParentPath().Should().Be(parentPath); @@ -315,5 +358,80 @@ namespace NzbDrone.Common.Test result[2].Should().Be(@"TV"); result[3].Should().Be(@"Series Title"); } + + [TestCase(@"C:\Test\")] + [TestCase(@"C:\Test")] + [TestCase(@"C:\Test\TV\")] + [TestCase(@"C:\Test\TV")] + public void IsPathValid_should_be_true(string path) + { + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue(); + } + + [TestCase(@"C:\Test \")] + [TestCase(@"C:\Test ")] + [TestCase(@"C:\ Test\")] + [TestCase(@"C:\ Test")] + [TestCase(@"C:\Test \TV")] + [TestCase(@"C:\ Test\TV")] + [TestCase(@"C:\Test \TV\")] + [TestCase(@"C:\ Test\TV\")] + [TestCase(@" C:\Test\TV\")] + [TestCase(@" C:\Test\TV")] + + public void IsPathValid_should_be_false_on_windows(string path) + { + WindowsOnly(); + path.IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); + } + + [TestCase(@"")] + [TestCase(@"relative/path")] + public void IsPathValid_should_be_false_on_unix(string path) + { + PosixOnly(); + path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); + } + + [TestCase(@"C:\", @"C:\")] + [TestCase(@"C:\\", @"C:\")] + [TestCase(@"C:\Test", @"C:\Test")] + [TestCase(@"C:\Test\", @"C:\Test")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\", @"\\server\share")] + public void windows_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } + + [TestCase("/", "/")] + [TestCase("//", "/")] + [TestCase("/test", "/test")] + [TestCase("/test/", "/test")] + [TestCase("/test//", "/test")] + public void unix_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } + + [TestCase(@"C:\Test\", @"C:\Test\Series Title", "Series Title")] + [TestCase(@"C:\Test\", @"C:\Test\Collection\Series Title", @"Collection\Series Title")] + [TestCase(@"C:\Test\mydir\", @"C:\Test\mydir\Collection\Series Title", @"Collection\Series Title")] + [TestCase(@"\\server\share", @"\\server\share\Series Title", "Series Title")] + [TestCase(@"\\server\share\mydir\", @"\\server\share\mydir\/Collection\Series Title", @"Collection\Series Title")] + public void windows_path_should_return_relative_path(string parentPath, string childPath, string relativePath) + { + parentPath.GetRelativePath(childPath).Should().Be(relativePath); + } + + [TestCase(@"/test", "/test/Series Title", "Series Title")] + [TestCase(@"/test/", "/test/Collection/Series Title", "Collection/Series Title")] + [TestCase(@"/test/mydir", "/test/mydir/Series Title", "Series Title")] + [TestCase(@"/test/mydir/", "/test/mydir/Collection/Series Title", "Collection/Series Title")] + [TestCase(@"/test/mydir/", @"/test/mydir/\Collection/Series Title", "Collection/Series Title")] + public void unix_path_should_return_relative_path(string parentPath, string childPath, string relativePath) + { + parentPath.GetRelativePath(childPath).Should().Be(relativePath); + } } } diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 9c348316f..237febe74 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Lifecycle; @@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test .AddNzbDroneLogger() .AutoAddServices(Bootstrap.ASSEMBLIES) .AddDummyDatabase() + .AddDummyLogDatabase() .AddStartupContext(new StartupContext("first", "second")); container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock>().Object); + container.RegisterInstance(new Mock>().Object); + container.RegisterInstance(new Mock>().Object); + container.RegisterInstance(new Mock>().Object); + container.RegisterInstance(new Mock>().Object); + container.RegisterInstance(new Mock>().Object); var serviceProvider = container.GetServiceProvider(); diff --git a/src/NzbDrone.Common/ArchiveService.cs b/src/NzbDrone.Common/ArchiveService.cs index 800d240ab..d420bbbc0 100644 --- a/src/NzbDrone.Common/ArchiveService.cs +++ b/src/NzbDrone.Common/ArchiveService.cs @@ -42,17 +42,18 @@ namespace NzbDrone.Common public void CreateZip(string path, IEnumerable files) { - using (var zipFile = ZipFile.Create(path)) + _logger.Debug("Creating archive {0}", path); + + using var zipFile = ZipFile.Create(path); + + zipFile.BeginUpdate(); + + foreach (var file in files) { - zipFile.BeginUpdate(); - - foreach (var file in files) - { - zipFile.Add(file, Path.GetFileName(file)); - } - - zipFile.CommitUpdate(); + zipFile.Add(file, Path.GetFileName(file)); } + + zipFile.CommitUpdate(); } private void ExtractZip(string compressedFile, string destination) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index d92c01d00..621d4b258 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); - return Directory.EnumerateDirectories(path); + return Directory.EnumerateDirectories(path, "*", new EnumerationOptions + { + AttributesToSkip = FileAttributes.System, + IgnoreInaccessible = true + }); } public IEnumerable GetFiles(string path, bool recursive) @@ -185,6 +189,25 @@ namespace NzbDrone.Common.Disk } var fi = new FileInfo(path); + + try + { + // If the file is a symlink, resolve the target path and get the size of the target file. + if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + var targetPath = fi.ResolveLinkTarget(true)?.FullName; + + if (targetPath != null) + { + fi = new FileInfo(targetPath); + } + } + } + catch (IOException ex) + { + Logger.Trace(ex, "Unable to resolve symlink target for {0}", path); + } + return fi.Length; } diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index f6f01fccf..30661d747 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -9,6 +10,8 @@ namespace NzbDrone.Common.Disk private readonly string _path; private readonly OsPathKind _kind; + private static readonly Regex UncPathRegex = new Regex(@"(?^\\\\(?:\?\\UNC\\)?[^\\]+\\[^\\]+)(?:\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public OsPath(string path) { if (path == null) @@ -96,6 +99,29 @@ namespace NzbDrone.Common.Disk return path; } + private static string TrimTrailingSlash(string path, OsPathKind kind) + { + switch (kind) + { + case OsPathKind.Windows when !path.EndsWith(":\\"): + while (!path.EndsWith(":\\") && path.EndsWith('\\')) + { + path = path[..^1]; + } + + return path; + case OsPathKind.Unix when path != "/": + while (path != "/" && path.EndsWith('/')) + { + path = path[..^1]; + } + + return path; + } + + return path; + } + public OsPathKind Kind => _kind; public bool IsWindowsPath => _kind == OsPathKind.Windows; @@ -130,7 +156,19 @@ namespace NzbDrone.Common.Disk if (index == -1) { - return new OsPath(null); + return Null; + } + + var rootLength = GetRootLength(); + + if (rootLength == _path.Length) + { + return Null; + } + + if (rootLength > index + 1) + { + return new OsPath(_path.Substring(0, rootLength)); } return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); @@ -139,6 +177,8 @@ namespace NzbDrone.Common.Disk public string FullPath => _path; + public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind); + public string FileName { get @@ -161,6 +201,29 @@ namespace NzbDrone.Common.Disk } } + public string Name + { + // Meant to behave similar to DirectoryInfo.Name + get + { + var index = GetFileNameIndex(); + + if (index == -1) + { + return PathWithoutTrailingSlash; + } + + var rootLength = GetRootLength(); + + if (rootLength > index + 1) + { + return _path.Substring(0, rootLength); + } + + return TrimTrailingSlash(_path.Substring(index).TrimStart('/', '\\'), _kind); + } + } + public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs); private int GetFileNameIndex() @@ -190,11 +253,50 @@ namespace NzbDrone.Common.Disk return index; } + private int GetRootLength() + { + if (!IsRooted) + { + return 0; + } + + if (_kind == OsPathKind.Unix) + { + return 1; + } + + if (_kind == OsPathKind.Windows) + { + if (HasWindowsDriveLetter(_path)) + { + return 3; + } + + var uncMatch = UncPathRegex.Match(_path); + + // \\?\UNC\server\share\ or \\server\share + if (uncMatch.Success) + { + return uncMatch.Groups["unc"].Length; + } + + // \\?\C:\ + if (_path.StartsWith(@"\\?\")) + { + return 7; + } + } + + return 0; + } + private string[] GetFragments() { return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); } + public static OsPath Null => new (null); + public override string ToString() { return _path; @@ -267,6 +369,11 @@ namespace NzbDrone.Common.Disk } public bool Equals(OsPath other) + { + return Equals(other, false); + } + + public bool Equals(OsPath other, bool ignoreTrailingSlash) { if (ReferenceEquals(other, null)) { @@ -278,8 +385,8 @@ namespace NzbDrone.Common.Disk return true; } - var left = _path; - var right = other._path; + var left = ignoreTrailingSlash ? PathWithoutTrailingSlash : _path; + var right = ignoreTrailingSlash ? other.PathWithoutTrailingSlash : other._path; if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) { diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index b9a206e4e..aece27859 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using NLog; @@ -25,22 +24,25 @@ namespace NzbDrone.Common.EnvironmentInfo static OsInfo() { - var platform = Environment.OSVersion.Platform; - - switch (platform) + if (OperatingSystem.IsWindows()) { - case PlatformID.Win32NT: - { - Os = Os.Windows; - break; - } - - case PlatformID.MacOSX: - case PlatformID.Unix: - { - Os = GetPosixFlavour(); - break; - } + Os = Os.Windows; + } + else if (OperatingSystem.IsMacOS()) + { + Os = Os.Osx; + } + else if (OperatingSystem.IsFreeBSD()) + { + Os = Os.Bsd; + } + else + { +#if ISMUSL + Os = Os.LinuxMusl; +#else + Os = Os.Linux; +#endif } } @@ -84,59 +86,6 @@ namespace NzbDrone.Common.EnvironmentInfo IsDocker = true; } } - - private static Os GetPosixFlavour() - { - var output = RunAndCapture("uname", "-s"); - - if (output.StartsWith("Darwin")) - { - return Os.Osx; - } - else if (output.Contains("BSD")) - { - return Os.Bsd; - } - else - { -#if ISMUSL - return Os.LinuxMusl; -#else - return Os.Linux; -#endif - } - } - - private static string RunAndCapture(string filename, string args) - { - var processStartInfo = new ProcessStartInfo - { - FileName = filename, - Arguments = args, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true - }; - - var output = string.Empty; - - try - { - using (var p = Process.Start(processStartInfo)) - { - // To avoid deadlocks, always read the output stream first and then wait. - output = p.StandardOutput.ReadToEnd(); - - p.WaitForExit(1000); - } - } - catch (Exception) - { - output = string.Empty; - } - - return output; - } } public interface IOsInfo diff --git a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs index 7feb431c4..cbc1f5f83 100644 --- a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs @@ -39,18 +39,24 @@ namespace NzbDrone.Common.Extensions private static bool IsLocalIPv4(byte[] ipv4Bytes) { // Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16) - bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; + var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; // Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) - bool IsClassA() => ipv4Bytes[0] == 10; + var isClassA = ipv4Bytes[0] == 10; // Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12) - bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; + var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; // Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16) - bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; + var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; - return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); + return isLinkLocal || isClassA || isClassC || isClassB; + } + + public static bool IsCgnatIpAddress(this IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + return bytes.Length == 4 && bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127; } } } diff --git a/src/NzbDrone.Common/Extensions/Int64Extensions.cs b/src/NzbDrone.Common/Extensions/NumberExtensions.cs similarity index 53% rename from src/NzbDrone.Common/Extensions/Int64Extensions.cs rename to src/NzbDrone.Common/Extensions/NumberExtensions.cs index bfca7f66c..15037b20b 100644 --- a/src/NzbDrone.Common/Extensions/Int64Extensions.cs +++ b/src/NzbDrone.Common/Extensions/NumberExtensions.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Globalization; namespace NzbDrone.Common.Extensions { - public static class Int64Extensions + public static class NumberExtensions { private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; @@ -26,5 +26,25 @@ namespace NzbDrone.Common.Extensions return string.Format(CultureInfo.InvariantCulture, "{0:n1} {1}", adjustedSize, SizeSuffixes[mag]); } + + public static long Megabytes(this int megabytes) + { + return Convert.ToInt64(megabytes * 1024L * 1024L); + } + + public static long Gigabytes(this int gigabytes) + { + return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); + } + + public static long Megabytes(this double megabytes) + { + return Convert.ToInt64(megabytes * 1024L * 1024L); + } + + public static long Gigabytes(this double gigabytes) + { + return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); + } } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 0c72a07f4..30a467f21 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; @@ -24,10 +25,14 @@ namespace NzbDrone.Common.Extensions private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Prowlarr.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; - private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(? path).IsNotNullOrWhiteSpace(); Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs); @@ -36,10 +41,10 @@ namespace NzbDrone.Common.Extensions // UNC if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\")) { - return info.FullName.TrimEnd('/', '\\', ' '); + return info.FullName.TrimEnd('/', '\\'); } - return info.FullName.TrimEnd('/').Trim('\\', ' '); + return info.FullName.TrimEnd('/').Trim('\\'); } public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null) @@ -80,55 +85,50 @@ namespace NzbDrone.Common.Extensions throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); } - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); + return childPath.Substring(parentPath.Length).Trim('\\', '/'); } public static string GetParentPath(this string childPath) { - var cleanPath = OsInfo.IsWindows - ? PARENT_PATH_END_SLASH_REGEX.Replace(childPath, "") - : childPath.TrimEnd(Path.DirectorySeparatorChar); + var path = new OsPath(childPath).Directory; - if (cleanPath.IsNullOrWhiteSpace()) - { - return null; - } + return path == OsPath.Null ? null : path.PathWithoutTrailingSlash; + } - return Directory.GetParent(cleanPath)?.FullName; + public static string GetParentName(this string childPath) + { + var path = new OsPath(childPath).Directory; + + return path == OsPath.Null ? null : path.Name; + } + + public static string GetDirectoryName(this string childPath) + { + var path = new OsPath(childPath); + + return path == OsPath.Null ? null : path.Name; } public static string GetCleanPath(this string path) { - var cleanPath = OsInfo.IsWindows - ? PARENT_PATH_END_SLASH_REGEX.Replace(path, "") - : path.TrimEnd(Path.DirectorySeparatorChar); + var osPath = new OsPath(path); - return cleanPath; + return osPath == OsPath.Null ? null : osPath.PathWithoutTrailingSlash; } public static bool IsParentPath(this string parentPath, string childPath) { - if (parentPath != "/" && !parentPath.EndsWith(":\\")) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - } + var parent = new OsPath(parentPath); + var child = new OsPath(childPath); - if (childPath != "/" && !parentPath.EndsWith(":\\")) + while (child.Directory != OsPath.Null) { - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - } - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison)) + if (child.Directory.Equals(parent, true)) { return true; } - child = child.Parent; + child = child.Directory; } return false; @@ -143,6 +143,27 @@ namespace NzbDrone.Common.Extensions return false; } + // Only check for leading or trailing spaces for path when running on Windows. + if (OsInfo.IsWindows) + { + if (path.Trim() != path) + { + return false; + } + + var directoryInfo = new DirectoryInfo(path); + + while (directoryInfo != null) + { + if (directoryInfo.Name.Trim() != directoryInfo.Name) + { + return false; + } + + directoryInfo = directoryInfo.Parent; + } + } + if (validationType == PathValidationType.AnyOs) { return IsPathValidForWindows(path) || IsPathValidForNonWindows(path); @@ -253,6 +274,11 @@ namespace NzbDrone.Common.Extensions return processName; } + public static string CleanPath(this string path) + { + return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray()); + } + public static string GetAppDataPath(this IAppFolderInfo appFolderInfo) { return appFolderInfo.AppDataFolder; diff --git a/src/NzbDrone.Common/Extensions/UrlExtensions.cs b/src/NzbDrone.Common/Extensions/UrlExtensions.cs index d71cfec15..fbe1832a8 100644 --- a/src/NzbDrone.Common/Extensions/UrlExtensions.cs +++ b/src/NzbDrone.Common/Extensions/UrlExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Web; namespace NzbDrone.Common.Extensions { @@ -18,5 +19,24 @@ namespace NzbDrone.Common.Extensions return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsWellFormedOriginalString(); } + + public static Uri RemoveQueryParam(this Uri url, string name) + { + var uriBuilder = new UriBuilder(url); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + + query.Remove(name); + uriBuilder.Query = query.ToString() ?? string.Empty; + + return uriBuilder.Uri; + } + + public static string GetQueryParam(this Uri url, string name) + { + var uriBuilder = new UriBuilder(url); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + + return query[name]; + } } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 1c22f9d16..29bb49fa5 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,13 +1,16 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -29,11 +32,14 @@ namespace NzbDrone.Common.Http.Dispatchers private readonly ICached _httpClientCache; private readonly ICached _credentialCache; + private readonly Logger _logger; + public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, ICertificateValidationService certificateValidationService, IUserAgentBuilder userAgentBuilder, - ICacheManager cacheManager) + ICacheManager cacheManager, + Logger logger) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; @@ -42,6 +48,8 @@ namespace NzbDrone.Common.Http.Dispatchers _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher)); _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); + + _logger = logger; } public async Task GetResponseAsync(HttpRequest request, CookieContainer cookies) @@ -275,7 +283,27 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } - private static async ValueTask onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + private bool HasRoutableIPv4Address() + { + // Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses + try + { + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + + return networkInterfaces.Any(ni => + ni.OperationalStatus == OperationalStatus.Up && + ni.GetIPProperties().UnicastAddresses.Any(ip => + ip.Address.AddressFamily == AddressFamily.InterNetwork && + !IPAddress.IsLoopback(ip.Address))); + } + catch (Exception e) + { + _logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message); + return true; + } + } + + private async ValueTask onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way. // This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6. @@ -298,10 +326,10 @@ namespace NzbDrone.Common.Http.Dispatchers } catch { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance) - // but in the interest of keeping this implementation simple, this is acceptable. - useIPv6 = false; + // Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections. + var routableIPv4 = HasRoutableIPv4Address(); + _logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled"); + useIPv6 = !routableIPv4; } finally { diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 470f4f5c4..326c30b04 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -109,7 +109,7 @@ namespace NzbDrone.Common.Http if (response.HasHttpRedirect && !RuntimeInfo.IsProduction) { - _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); + _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.RedirectUrl); } if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode))) diff --git a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs index 022d8adee..c80044d29 100644 --- a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs +++ b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs @@ -30,7 +30,8 @@ namespace NzbDrone.Common.Http.Proxy { if (!string.IsNullOrWhiteSpace(BypassFilter)) { - var hostlist = BypassFilter.Split(','); + var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for (var i = 0; i < hostlist.Length; i++) { if (hostlist[i].StartsWith("*")) diff --git a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs index e03161702..e7ab0126d 100644 --- a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs @@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http { data = new XElement("base64", Convert.ToBase64String(bytes)); } + else if (value is Dictionary d) + { + data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value)))); + } else { throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}"); diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index 38ff304a1..393d6613a 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Common.Instrumentation private static readonly Regex[] CleansingRules = { // Url - new (@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|rsskey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=[?& ;])[^=]*?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"torrentleech\.org/rss/download/[0-9]+/(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), @@ -21,6 +21,7 @@ namespace NzbDrone.Common.Instrumentation new (@"(?<=authkey = "")(?[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"(?:sharewood)\.[a-z]{2,3}/api/(?[a-z0-9]{16,})/", RegexOptions.Compiled | RegexOptions.IgnoreCase), // UNIT3D new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), @@ -61,6 +62,9 @@ namespace NzbDrone.Common.Instrumentation // Applications new (@"""name"":""apikey"",""value"":""(?[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + + // Discord + new (@"discord.com/api/webhooks/((?[\w-]+)/)?(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(? - { - c.ForLogger("System.*").WriteToNil(LogLevel.Warn); - c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); - c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); - c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); - }); - } - private static void RegisterConsole() { var level = LogLevel.Trace; @@ -122,7 +115,12 @@ namespace NzbDrone.Common.Instrumentation var coloredConsoleTarget = new ColoredConsoleTarget(); coloredConsoleTarget.Name = "consoleLogger"; - coloredConsoleTarget.Layout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; + + var logFormat = Enum.TryParse(Environment.GetEnvironmentVariable("PROWLARR__LOG__CONSOLEFORMAT"), out var formatEnumValue) + ? formatEnumValue + : ConsoleLogFormat.Standard; + + ConfigureConsoleLayout(coloredConsoleTarget, logFormat); var loggingRule = new LoggingRule("*", level, coloredConsoleTarget); @@ -139,7 +137,7 @@ namespace NzbDrone.Common.Instrumentation private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel) { - var fileTarget = new NzbDroneFileTarget(); + var fileTarget = new CleansingFileTarget(); fileTarget.Name = name; fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName); @@ -148,11 +146,11 @@ namespace NzbDrone.Common.Instrumentation fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttempts = 10; - fileTarget.ArchiveAboveSize = 1024000; + fileTarget.ArchiveAboveSize = 1.Megabytes(); fileTarget.MaxArchiveFiles = maxArchiveFiles; fileTarget.EnableFileDelete = true; fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling; - fileTarget.Layout = FILE_LOG_LAYOUT; + fileTarget.Layout = FileLogLayout; var loggingRule = new LoggingRule("*", minLogLevel, fileTarget); @@ -171,7 +169,7 @@ namespace NzbDrone.Common.Instrumentation fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttempts = 100; - fileTarget.Layout = FILE_LOG_LAYOUT; + fileTarget.Layout = FileLogLayout; var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget); @@ -196,6 +194,17 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration.LoggingRules.Insert(0, rule); } + private static void RegisterGlobalFilters() + { + LogManager.Setup().LoadConfiguration(c => + { + c.ForLogger("System.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); + c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); + }); + } + public static Logger GetLogger(Type obj) { return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", "")); @@ -205,5 +214,20 @@ namespace NzbDrone.Common.Instrumentation { return GetLogger(obj.GetType()); } + + public static void ConfigureConsoleLayout(ColoredConsoleTarget target, ConsoleLogFormat format) + { + target.Layout = format switch + { + ConsoleLogFormat.Clef => NzbDroneLogger.ClefLogLayout, + _ => NzbDroneLogger.CleansingConsoleLayout + }; + } + } + + public enum ConsoleLogFormat + { + Standard, + Clef } } diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index fb0fa1ce5..3a4737f74 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -106,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry public bool FilterEvents { get; set; } public bool SentryEnabled { get; set; } - public SentryTarget(string dsn) + public SentryTarget(string dsn, IAppFolderInfo appFolderInfo) { _sdk = SentrySdk.Init(o => { @@ -114,9 +114,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry o.AttachStacktrace = true; o.MaxBreadcrumbs = 200; o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}"; - o.BeforeSend = x => SentryCleanser.CleanseEvent(x); - o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); + o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x)); + o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x)); o.Environment = BuildInfo.Branch; + + // Crash free run statistics (sends a ping for healthy and for crashes sessions) + o.AutoSessionTracking = false; + + // Caches files in the event device is offline + // Sentry creates a 'sentry' sub directory, no need to concat here + o.CacheDirectoryPath = appFolderInfo.GetAppDataPath(); + + // default environment is production + if (!RuntimeInfo.IsProduction) + { + if (RuntimeInfo.IsDevelopment) + { + o.Environment = "development"; + } + else if (RuntimeInfo.IsTesting) + { + o.Environment = "testing"; + } + else + { + o.Environment = "other"; + } + } }); InitializeScope(); @@ -124,7 +148,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry _debounce = new SentryDebounce(); // initialize to true and reconfigure later - // Otherwise it will default to false and any errors occuring + // Otherwise it will default to false and any errors occurring // before config file gets read will not be filtered FilterEvents = true; SentryEnabled = true; @@ -134,7 +158,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry { SentrySdk.ConfigureScope(scope => { - scope.User = new User + scope.User = new SentryUser { Id = HashUtil.AnonymousToken() }; @@ -183,9 +207,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry private void OnError(Exception ex) { - var webException = ex as WebException; - - if (webException != null) + if (ex is WebException webException) { var response = webException.Response as HttpWebResponse; var statusCode = response?.StatusCode; @@ -317,13 +339,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry } } + var level = LoggingLevelMap[logEvent.Level]; var sentryEvent = new SentryEvent(logEvent.Exception) { - Level = LoggingLevelMap[logEvent.Level], + Level = level, Logger = logEvent.LoggerName, Message = logEvent.FormattedMessage }; + if (level is SentryLevel.Fatal && logEvent.Exception is not null) + { + // Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with + // the 'unhandled' exception flag + logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false); + } + sentryEvent.SetExtras(extras); sentryEvent.SetFingerprint(fingerPrint); diff --git a/src/NzbDrone.Common/Options/AppOptions.cs b/src/NzbDrone.Common/Options/AppOptions.cs new file mode 100644 index 000000000..74cdf1d29 --- /dev/null +++ b/src/NzbDrone.Common/Options/AppOptions.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Common.Options; + +public class AppOptions +{ + public string InstanceName { get; set; } + public string Theme { get; set; } + public bool? LaunchBrowser { get; set; } +} diff --git a/src/NzbDrone.Common/Options/AuthOptions.cs b/src/NzbDrone.Common/Options/AuthOptions.cs new file mode 100644 index 000000000..64330b68b --- /dev/null +++ b/src/NzbDrone.Common/Options/AuthOptions.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Common.Options; + +public class AuthOptions +{ + public string ApiKey { get; set; } + public bool? Enabled { get; set; } + public string Method { get; set; } + public string Required { get; set; } + public bool? TrustCgnatIpAddresses { get; set; } +} diff --git a/src/NzbDrone.Common/Options/LogOptions.cs b/src/NzbDrone.Common/Options/LogOptions.cs new file mode 100644 index 000000000..6460eeaa6 --- /dev/null +++ b/src/NzbDrone.Common/Options/LogOptions.cs @@ -0,0 +1,17 @@ +namespace NzbDrone.Common.Options; + +public class LogOptions +{ + public string Level { get; set; } + public bool? FilterSentryEvents { get; set; } + public int? Rotate { get; set; } + public int? SizeLimit { get; set; } + public bool? Sql { get; set; } + public string ConsoleLevel { get; set; } + public string ConsoleFormat { get; set; } + public bool? AnalyticsEnabled { get; set; } + public string SyslogServer { get; set; } + public int? SyslogPort { get; set; } + public string SyslogLevel { get; set; } + public bool? DbEnabled { get; set; } +} diff --git a/src/NzbDrone.Common/Options/ServerOptions.cs b/src/NzbDrone.Common/Options/ServerOptions.cs new file mode 100644 index 000000000..d21e12b2a --- /dev/null +++ b/src/NzbDrone.Common/Options/ServerOptions.cs @@ -0,0 +1,12 @@ +namespace NzbDrone.Common.Options; + +public class ServerOptions +{ + public string UrlBase { get; set; } + public string BindAddress { get; set; } + public int? Port { get; set; } + public bool? EnableSsl { get; set; } + public int? SslPort { get; set; } + public string SslCertPath { get; set; } + public string SslCertPassword { get; set; } +} diff --git a/src/NzbDrone.Common/Options/UpdateOptions.cs b/src/NzbDrone.Common/Options/UpdateOptions.cs new file mode 100644 index 000000000..a8eaad8fb --- /dev/null +++ b/src/NzbDrone.Common/Options/UpdateOptions.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.Options; + +public class UpdateOptions +{ + public string Mechanism { get; set; } + public bool? Automatically { get; set; } + public string ScriptPath { get; set; } + public string Branch { get; set; } +} diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 5db0565e0..c68207a09 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Model; @@ -117,7 +118,9 @@ namespace NzbDrone.Common.Processes UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, - RedirectStandardInput = true + RedirectStandardInput = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 }; if (environmentVariables != null) @@ -313,7 +316,7 @@ namespace NzbDrone.Common.Processes processInfo = new ProcessInfo(); processInfo.Id = process.Id; processInfo.Name = process.ProcessName; - processInfo.StartPath = process.MainModule.FileName; + processInfo.StartPath = process.MainModule?.FileName; if (process.Id != GetCurrentProcessId() && process.HasExited) { diff --git a/src/NzbDrone.Common/Prowlarr.Common.csproj b/src/NzbDrone.Common/Prowlarr.Common.csproj index 5210a351a..106890399 100644 --- a/src/NzbDrone.Common/Prowlarr.Common.csproj +++ b/src/NzbDrone.Common/Prowlarr.Common.csproj @@ -5,18 +5,21 @@ - - + + + - - - - + + + + + - + + - + diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs new file mode 100644 index 000000000..79d0adaee --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs @@ -0,0 +1,43 @@ +using System; +using System.Data.SQLite; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Converters; + +[TestFixture] +public class TimeSpanConverterFixture : CoreTest +{ + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + + [Test] + public void should_return_string_when_saving_timespan_to_db() + { + var span = TimeSpan.FromMilliseconds(10); + + Subject.SetValue(_param, span); + _param.Value.Should().Be(span.ToString()); + } + + [Test] + public void should_return_timespan_when_getting_string_from_db() + { + var span = TimeSpan.FromMilliseconds(10); + + Subject.Parse(span.ToString()).Should().Be(span); + } + + [Test] + public void should_return_zero_timespan_for_db_null_value_when_getting_from_db() + { + Subject.Parse(null).Should().Be(TimeSpan.Zero); + } +} diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs new file mode 100644 index 000000000..05bf04fea --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Test.Datastore; + +[TestFixture] +public class DatabaseVersionParserFixture +{ + [TestCase("3.44.2", 3, 44, 2)] + public void should_parse_sqlite_database_version(string serverVersion, int majorVersion, int minorVersion, int buildVersion) + { + var version = DatabaseVersionParser.ParseServerVersion(serverVersion); + + version.Should().NotBeNull(); + version.Major.Should().Be(majorVersion); + version.Minor.Should().Be(minorVersion); + version.Build.Should().Be(buildVersion); + } + + [TestCase("14.8 (Debian 14.8-1.pgdg110+1)", 14, 8, null)] + [TestCase("16.3 (Debian 16.3-1.pgdg110+1)", 16, 3, null)] + [TestCase("16.3 - Percona Distribution", 16, 3, null)] + [TestCase("17.0 - Percona Server", 17, 0, null)] + public void should_parse_postgres_database_version(string serverVersion, int majorVersion, int minorVersion, int? buildVersion) + { + var version = DatabaseVersionParser.ParseServerVersion(serverVersion); + + version.Should().NotBeNull(); + version.Major.Should().Be(majorVersion); + version.Minor.Should().Be(minorVersion); + + if (buildVersion.HasValue) + { + version.Build.Should().Be(buildVersion.Value); + } + } +} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs new file mode 100644 index 000000000..513095162 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Notifications.Email; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class email_encryptionFixture : MigrationTest + { + [Test] + public void should_convert_do_not_require_encryption_to_auto() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + Name = "Mail Prowlarr", + Implementation = "Email", + Tags = "[]", + Settings = new EmailSettings38 + { + Server = "smtp.gmail.com", + Port = 563, + To = new List { "dont@email.me" }, + RequireEncryption = false + }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred); + } + + [Test] + public void should_convert_require_encryption_to_always() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + Name = "Mail Prowlarr", + Implementation = "Email", + Tags = "[]", + Settings = new EmailSettings38 + { + Server = "smtp.gmail.com", + Port = 563, + To = new List { "dont@email.me" }, + RequireEncryption = true + }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Always); + } + + [Test] + public void should_use_defaults_when_settings_are_empty() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + Name = "Mail Prowlarr", + Implementation = "Email", + Tags = "[]", + Settings = new { }.ToJson(), + ConfigContract = "EmailSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Email"); + items.First().ConfigContract.Should().Be("EmailSettings"); + items.First().Settings.UseEncryption.Should().Be((int)EmailEncryptionType.Preferred); + } + } + + public class NotificationDefinition39 + { + public int Id { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public EmailSettings39 Settings { get; set; } + public string Name { get; set; } + public bool OnGrab { get; set; } + public bool OnHealthIssue { get; set; } + public bool OnHealthRestored { get; set; } + public bool OnApplicationUpdate { get; set; } + public bool SupportsOnGrab { get; set; } + public bool IncludeManualGrabs { get; set; } + public bool SupportsOnHealthIssue { get; set; } + public bool SupportsOnHealthRestored { get; set; } + public bool IncludeHealthWarnings { get; set; } + public bool SupportsOnApplicationUpdate { get; set; } + public List Tags { get; set; } + } + + public class EmailSettings38 + { + public string Server { get; set; } + public int Port { get; set; } + public bool RequireEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public IEnumerable To { get; set; } + public IEnumerable Cc { get; set; } + public IEnumerable Bcc { get; set; } + } + + public class EmailSettings39 + { + public string Server { get; set; } + public int Port { get; set; } + public int UseEncryption { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string From { get; set; } + public IEnumerable To { get; set; } + public IEnumerable Cc { get; set; } + public IEnumerable Bcc { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/040_newznab_category_to_capabilities_settingsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/040_newznab_category_to_capabilities_settingsFixture.cs new file mode 100644 index 000000000..b51bf6433 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/040_newznab_category_to_capabilities_settingsFixture.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class newznab_category_to_capabilities_settingsFixture : MigrationTest + { + [Test] + public void should_migrate_categories_when_capabilities_is_not_defined() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Indexers").Row(new + { + Name = "Usenet Indexer", + Redirect = false, + AppProfileId = 0, + DownloadClientId = 0, + Priority = 25, + Added = DateTime.UtcNow, + Implementation = "Newznab", + Settings = new + { + Categories = new[] + { + new { Id = 2000, Name = "Movies" }, + new { Id = 5000, Name = "TV" } + } + }.ToJson(), + ConfigContract = "NewznabSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Newznab"); + items.First().ConfigContract.Should().Be("NewznabSettings"); + items.First().Settings.Should().ContainKey("capabilities"); + items.First().Settings.Should().NotContainKey("categories"); + + var newznabSettings = items.First().Settings.ToObject(); + newznabSettings.Capabilities.Should().NotBeNull(); + newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false); + newznabSettings.Capabilities.Categories.Should().HaveCount(2); + newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies"); + newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV"); + } + + [Test] + public void should_migrate_categories_when_capabilities_is_defined() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Indexers").Row(new + { + Name = "Usenet Indexer", + Redirect = false, + AppProfileId = 0, + DownloadClientId = 0, + Priority = 25, + Added = DateTime.UtcNow, + Implementation = "Newznab", + Settings = new + { + Capabilities = new + { + SupportsRawSearch = true + }, + Categories = new[] + { + new { Id = 2000, Name = "Movies" }, + new { Id = 5000, Name = "TV" } + } + }.ToJson(), + ConfigContract = "NewznabSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Newznab"); + items.First().ConfigContract.Should().Be("NewznabSettings"); + items.First().Settings.Should().ContainKey("capabilities"); + items.First().Settings.Should().NotContainKey("categories"); + + var newznabSettings = items.First().Settings.ToObject(); + newznabSettings.Capabilities.Should().NotBeNull(); + newznabSettings.Capabilities.SupportsRawSearch.Should().Be(true); + newznabSettings.Capabilities.Categories.Should().HaveCount(2); + newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 2000 && c.Name == "Movies"); + newznabSettings.Capabilities.Categories.Should().Contain(c => c.Id == 5000 && c.Name == "TV"); + } + + [Test] + public void should_use_defaults_when_categories_are_empty() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Indexers").Row(new + { + Name = "Usenet Indexer", + Redirect = false, + AppProfileId = 0, + DownloadClientId = 0, + Priority = 25, + Added = DateTime.UtcNow, + Implementation = "Newznab", + Settings = new + { + Categories = Array.Empty() + }.ToJson(), + ConfigContract = "NewznabSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Newznab"); + items.First().ConfigContract.Should().Be("NewznabSettings"); + items.First().Settings.Should().ContainKey("capabilities"); + items.First().Settings.Should().NotContainKey("categories"); + + var newznabSettings = items.First().Settings.ToObject(); + newznabSettings.Capabilities.Should().NotBeNull(); + newznabSettings.Capabilities.SupportsRawSearch.Should().Be(false); + newznabSettings.Capabilities.Categories.Should().NotBeNull(); + newznabSettings.Capabilities.Categories.Should().HaveCount(0); + } + + [Test] + public void should_use_defaults_when_settings_are_empty() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Indexers").Row(new + { + Name = "Usenet Indexer", + Redirect = false, + AppProfileId = 0, + DownloadClientId = 0, + Priority = 25, + Added = DateTime.UtcNow, + Implementation = "Newznab", + Settings = new { }.ToJson(), + ConfigContract = "NewznabSettings" + }); + }); + + var items = db.Query("SELECT \"Id\", \"Implementation\", \"ConfigContract\", \"Settings\" FROM \"Indexers\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Newznab"); + items.First().ConfigContract.Should().Be("NewznabSettings"); + items.First().Settings.Should().NotContainKey("capabilities"); + items.First().Settings.Should().NotContainKey("categories"); + items.First().Settings.ToObject().Capabilities.Should().BeNull(); + } + } + + public class IndexerDefinition40 + { + public int Id { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public JObject Settings { get; set; } + } + + public class NewznabSettings39 + { + public object Categories { get; set; } + } + + public class NewznabSettings40 + { + public NewznabCapabilitiesSettings40 Capabilities { get; set; } + } + + public class NewznabCapabilitiesSettings40 + { + public bool SupportsRawSearch { get; set; } + public List Categories { get; set; } + } + + public class IndexerCategory40 + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index 6dbd46c31..3e6a8f66e 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework Mocker.SetConstant(new HttpProxySettingsProvider(Mocker.Resolve())); Mocker.SetConstant(new ManagedWebProxyFactory(Mocker.Resolve())); Mocker.SetConstant(new X509CertificateValidationService(Mocker.Resolve(), TestLogger)); - Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve())); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new HttpClient(Array.Empty(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new ProwlarrCloudRequestBuilder()); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs index 64eeb9169..7d859eb9d 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Localization; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Update; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.HealthCheck.Checks { @@ -21,28 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Returns("Some Warning Message"); } - [Test] - public void should_return_error_when_app_folder_is_write_protected() - { - WindowsOnly(); - - Mocker.GetMock() - .Setup(s => s.StartUpFolder) - .Returns(@"C:\NzbDrone"); - - Mocker.GetMock() - .Setup(c => c.FolderWritable(It.IsAny())) - .Returns(false); - - Subject.Check().ShouldBeError(); - } - [Test] public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled() { - PosixOnly(); - - const string startupFolder = @"/opt/nzbdrone"; + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -62,10 +45,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled() { - PosixOnly(); - - const string startupFolder = @"/opt/nzbdrone"; - const string uiFolder = @"/opt/nzbdrone/UI"; + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); + var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic(); Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -89,7 +70,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled() { - PosixOnly(); + var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -101,7 +82,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock() .Setup(s => s.StartUpFolder) - .Returns(@"/opt/nzbdrone"); + .Returns(startupFolder); Mocker.GetMock() .Verify(c => c.FolderWritable(It.IsAny()), Times.Never()); diff --git a/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs b/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs index 067149904..2beeb16f9 100644 --- a/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Test.Http { private HttpProxySettings GetProxySettings() { - return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null); + return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null); } [Test] @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Http Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue(); + Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue(); } [Test] @@ -31,6 +32,7 @@ namespace NzbDrone.Core.Test.Http var settings = GetProxySettings(); Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse(); + Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs index be01ec6e4..ae7eaa762 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs @@ -45,10 +45,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests var releases = (await Subject.Fetch(new BasicSearchCriteria { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases; - releases.Should().HaveCount(33); + releases.Should().HaveCount(39); releases.First().Should().BeOfType(); - var firstTorrentInfo = releases.ElementAt(2) as TorrentInfo; + var firstTorrentInfo = releases.ElementAt(3) as TorrentInfo; firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]"); firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests firstTorrentInfo.Files.Should().Be(1); firstTorrentInfo.MinimumSeedTime.Should().Be(259200); - var secondTorrentInfo = releases.ElementAt(16) as TorrentInfo; + var secondTorrentInfo = releases.ElementAt(20) as TorrentInfo; secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]"); secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests secondTorrentInfo.Files.Should().Be(22); secondTorrentInfo.MinimumSeedTime.Should().Be(655200); - var thirdTorrentInfo = releases.ElementAt(18) as TorrentInfo; + var thirdTorrentInfo = releases.ElementAt(23) as TorrentInfo; thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]"); thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests thirdTorrentInfo.Files.Should().Be(1); thirdTorrentInfo.MinimumSeedTime.Should().Be(475200); - var fourthTorrentInfo = releases.ElementAt(3) as TorrentInfo; + var fourthTorrentInfo = releases.ElementAt(5) as TorrentInfo; fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]"); fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); @@ -120,9 +120,9 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests fourthTorrentInfo.Files.Should().Be(1); fourthTorrentInfo.MinimumSeedTime.Should().Be(259200); - var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo; + var fifthTorrentInfo = releases.ElementAt(28) as TorrentInfo; - fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]"); + fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS 2021 S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]"); fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); fifthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/944509/download/somepass"); fifthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/944509/group"); @@ -138,7 +138,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests fifthTorrentInfo.Files.Should().Be(11); fifthTorrentInfo.MinimumSeedTime.Should().Be(529200); - var sixthTorrentInfo = releases.ElementAt(31) as TorrentInfo; + var sixthTorrentInfo = releases.ElementAt(37) as TorrentInfo; sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]"); sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs index b8f7951d2..dcabb4dbf 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/AvistazFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21")); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21")); torrentInfo.Size.Should().Be(935127615); torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2"); torrentInfo.MagnetUrl.Should().Be(null); diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs index 4c4f9f38e..fe4ada593 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 10:04:50")); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 09:04:50")); torrentInfo.Size.Should().Be(7085405541); torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf"); torrentInfo.MagnetUrl.Should().Be(null); diff --git a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs index e841659fd..f0531e9ec 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/PrivateHDFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49")); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49")); torrentInfo.Size.Should().Be(69914591044); torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2"); torrentInfo.MagnetUrl.Should().Be(null); diff --git a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs index 10a4c8145..39d5a6f61 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests query.Tvrage.Should().BeNull(); query.Search.Should().BeNull(); query.Category.Should().Be("Episode"); - query.Name.Should().Be("S01E03"); + query.Name.Should().Be("S01E03%"); } [Test] @@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests 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("S02E03%"); } [Test] diff --git a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs index 3699a9d0c..b6f06d180 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/FileListTests/FileListFixture.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19")); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19")); torrentInfo.Size.Should().Be(8300512414); torrentInfo.InfoHash.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null); diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs index 5ff5b5ee9..4bc704ccd 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; @@ -21,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests [SetUp] public void Setup() { - Subject.Definition = new IndexerDefinition() + Subject.Definition = new IndexerDefinition { Name = "GazelleGames", - Settings = new GazelleGamesSettings() { Apikey = "somekey" } + Settings = new GazelleGamesSettings { Apikey = "somekey" } }; } @@ -37,20 +38,20 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests .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))); - var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; - releases.Should().HaveCount(1464); + releases.Should().HaveCount(1462); releases.First().Should().BeOfType(); var torrentInfo = releases.First() as TorrentInfo; - torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM"); + torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM (2020) [Windows / Multi-Language / Full ISO]"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadUrl.Should().Be("https://gazellegames.net/torrents.php?action=download&id=303216&authkey=prowlarr&torrent_pass="); torrentInfo.InfoUrl.Should().Be("https://gazellegames.net/torrents.php?id=84781&torrentid=303216"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime()); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 06:39:11", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); torrentInfo.Size.Should().Be(80077617780); torrentInfo.InfoHash.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null); @@ -74,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests .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))); - var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; releases.Should().HaveCount(0); } diff --git a/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs index 39d628d79..06326d162 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/HDBitsTests/HDBitsFixture.cs @@ -26,15 +26,15 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests [SetUp] public void Setup() { - Subject.Definition = new IndexerDefinition() + Subject.Definition = new IndexerDefinition { Name = "HdBits", - Settings = new HDBitsSettings() { ApiKey = "fakekey" } + Settings = new HDBitsSettings { ApiKey = "fakekey" } }; _movieSearchCriteria = new MovieSearchCriteria { - Categories = new int[] { 2000, 2010 }, + Categories = new[] { 2000, 2010 }, ImdbId = "0076759" }; } @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases; torrents.Should().HaveCount(2); - torrents.First().Should().BeOfType(); + torrents.First().Should().BeOfType(); var first = torrents.First() as TorrentInfo; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index 231c4bb56..084767ffa 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -33,6 +33,10 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests }; _caps = new IndexerCapabilities(); + + _caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies"); + _caps.Categories.AddCategoryMapping(5000, NewznabStandardCategory.TV, "TV"); + Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny(), It.IsAny())) .Returns(_caps); diff --git a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs index 1ba9c0af1..d33dde2d3 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests var torrentInfo = releases.First() as TorrentInfo; - torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]"); + torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [Album] [2.0 Mix 2019] [MP3 V2 (VBR) / BD]"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448"); torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448"); diff --git a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs index 7a7cc836f..d7eb35cd1 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs @@ -44,10 +44,10 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests var torrentInfo = releases.First() as TorrentInfo; - torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication [1999] [Album] [US / Reissue 2020] [FLAC 24bit Lossless] [Vinyl]"); + torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication (1999) [Album] [US / Reissue 2020] [FLAC 24bit Lossless / Vinyl]"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313"); - torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313"); + torrentInfo.DownloadUrl.Should().Be("https://redacted.sh/ajax.php?action=download&id=3892313"); + torrentInfo.InfoUrl.Should().Be("https://redacted.sh/torrents.php?id=16720&torrentid=3892313"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-17 08:02:35")); diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index e5a6f6c32..a5fdf0506 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -37,6 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests _caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies"); _caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD"); + _caps.Categories.AddCategoryMapping(5000, NewznabStandardCategory.TV, "TV"); + _caps.Categories.AddCategoryMapping(5040, NewznabStandardCategory.TVHD, "TV/HD"); + _caps.Categories.AddCategoryMapping(5070, NewznabStandardCategory.TVAnime, "TV/Anime"); Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny(), It.IsAny())) @@ -83,23 +86,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; - releases.Should().HaveCount(5); - - releases.First().Should().BeOfType(); - var releaseInfo = releases.First() as TorrentInfo; - - releaseInfo.Title.Should().Be("Series Title S05E02 HDTV x264-Xclusive [eztv]"); - releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - releaseInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969"); - releaseInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969"); - releaseInfo.InfoUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D"); - releaseInfo.CommentUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D"); - releaseInfo.Indexer.Should().Be(Subject.Definition.Name); - releaseInfo.PublishDate.Should().Be(DateTime.Parse("Sat, 11 Apr 2015 21:34:00 -0600").ToUniversalTime()); - releaseInfo.Size.Should().Be(388895872); - releaseInfo.InfoHash.Should().Be("9fb267cff5ae5603f07a347676ec3bf3e35f75e1"); - releaseInfo.Seeders.Should().Be(34128); - releaseInfo.Peers.Should().Be(36724); + releases.Should().HaveCount(0); } [Test] diff --git a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs new file mode 100644 index 000000000..309169c69 --- /dev/null +++ b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs @@ -0,0 +1,111 @@ +using System; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Notifications.Email; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.NotificationTests.EmailTests +{ + [TestFixture] + public class EmailSettingsValidatorFixture : CoreTest + { + private EmailSettings _emailSettings; + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _validator = new TestValidator + { + v => v.RuleFor(s => s).SetValidator(Subject) + }; + + _emailSettings = Builder.CreateNew() + .With(s => s.Server = "someserver") + .With(s => s.Port = 567) + .With(s => s.UseEncryption = (int)EmailEncryptionType.Always) + .With(s => s.From = "dont@email.me") + .With(s => s.To = new string[] { "dont@email.me" }) + .Build(); + } + + [Test] + public void should_be_valid_if_all_settings_valid() + { + _validator.Validate(_emailSettings).IsValid.Should().BeTrue(); + } + + [Test] + public void should_not_be_valid_if_port_is_out_of_range() + { + _emailSettings.Port = 900000; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_server_is_empty() + { + _emailSettings.Server = ""; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_from_is_empty() + { + _emailSettings.From = ""; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("prowlarr")] + [TestCase("email.me")] + [Ignore("Allowed coz some email servers allow arbitrary source, we probably need to support 'Name ' syntax")] + public void should_not_be_valid_if_from_is_invalid(string email) + { + _emailSettings.From = email; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("prowlarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_to_is_invalid(string email) + { + _emailSettings.To = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("prowlarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_cc_is_invalid(string email) + { + _emailSettings.Cc = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [TestCase("prowlarr")] + [TestCase("email.me")] + public void should_not_be_valid_if_bcc_is_invalid(string email) + { + _emailSettings.Bcc = new string[] { email }; + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_to_bcc_cc_are_all_empty() + { + _emailSettings.To = Array.Empty(); + _emailSettings.Cc = Array.Empty(); + _emailSettings.Bcc = Array.Empty(); + + _validator.Validate(_emailSettings).IsValid.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj index 854825b1f..13bc15cbc 100644 --- a/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj @@ -3,10 +3,10 @@ net6.0 - + - + diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index ce2c446e6..5f2aa2662 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -35,18 +35,18 @@ namespace NzbDrone.Core.Test.UpdateTests { _updatePackage = new UpdatePackage { - FileName = "NzbDrone.develop.2.0.0.0.tar.gz", + FileName = "NzbDrone.develop.1.0.0.0.tar.gz", Url = "http://download.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz", - Version = new Version("2.0.0.0") + Version = new Version("1.0.0.0") }; } else { _updatePackage = new UpdatePackage { - FileName = "NzbDrone.develop.2.0.0.0.zip", + FileName = "NzbDrone.develop.1.0.0.0.zip", Url = "http://download.sonarr.tv/v2/develop/windows/NzbDrone.develop.zip", - Version = new Version("2.0.0.0") + Version = new Version("1.0.0.0") }; } @@ -90,17 +90,6 @@ namespace NzbDrone.Core.Test.UpdateTests .Returns(true); } - [Test] - public void should_not_update_if_inside_docker() - { - Mocker.GetMock().Setup(x => x.IsDocker).Returns(true); - - Subject.Execute(new ApplicationUpdateCommand()); - - Mocker.GetMock() - .Verify(c => c.Start(It.IsAny(), It.Is(s => s.StartsWith("12")), null, null, null), Times.Never()); - } - [Test] public void should_delete_sandbox_before_update_if_folder_exists() { @@ -338,6 +327,28 @@ namespace NzbDrone.Core.Test.UpdateTests .Verify(v => v.SaveConfigDictionary(It.Is>(d => d.ContainsKey("Branch") && (string)d["Branch"] == "fake")), Times.Once()); } + [Test] + public void should_not_update_with_built_in_updater_inside_docker_container() + { + Mocker.GetMock().Setup(x => x.PackageUpdateMechanism).Returns(UpdateMechanism.Docker); + + Subject.Execute(new ApplicationUpdateCommand()); + + Mocker.GetMock() + .Verify(c => c.Start(It.IsAny(), It.Is(s => s.StartsWith("12")), null, null, null), Times.Never()); + } + + [Test] + public void should_not_update_with_built_in_updater_when_external_updater_is_configured() + { + Mocker.GetMock().Setup(x => x.IsExternalUpdateMechanism).Returns(true); + + Subject.Execute(new ApplicationUpdateCommand()); + + Mocker.GetMock() + .Verify(c => c.Start(It.IsAny(), It.Is(s => s.StartsWith("12")), null, null, null), Times.Never()); + } + [TearDown] public void TearDown() { diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index d35409c0b..8d31502fa 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations public string Hint { get; set; } } + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class FieldTokenAttribute : Attribute + { + public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null) + { + Label = label; + Field = field; + Token = token; + Value = value?.ToString(); + } + + public string Label { get; set; } + public TokenField Field { get; set; } + public string Token { get; set; } + public string Value { get; set; } + } + public class FieldSelectOption { public int Value { get; set; } @@ -82,4 +99,11 @@ namespace NzbDrone.Core.Annotations ApiKey, UserName } + + public enum TokenField + { + Label, + HelpText, + HelpTextWarning + } } diff --git a/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs b/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs index ff966b196..349e50622 100644 --- a/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs +++ b/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs @@ -10,9 +10,9 @@ namespace NzbDrone.Core.Applications void DeleteAllForApp(int appId); } - public class TagRepository : BasicRepository, IAppIndexerMapRepository + public class AppIndexerMapRepository : BasicRepository, IAppIndexerMapRepository { - public TagRepository(IMainDatabase database, IEventAggregator eventAggregator) + public AppIndexerMapRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { } diff --git a/src/NzbDrone.Core/Applications/ApplicationBase.cs b/src/NzbDrone.Core/Applications/ApplicationBase.cs index e055d37aa..b4d32f054 100644 --- a/src/NzbDrone.Core/Applications/ApplicationBase.cs +++ b/src/NzbDrone.Core/Applications/ApplicationBase.cs @@ -11,6 +11,8 @@ namespace NzbDrone.Core.Applications public abstract class ApplicationBase : IApplication where TSettings : IProviderConfig, new() { + private readonly IIndexerFactory _indexerFactory; + protected readonly IAppIndexerMapService _appIndexerMapService; protected readonly Logger _logger; @@ -27,9 +29,10 @@ namespace NzbDrone.Core.Applications protected TSettings Settings => (TSettings)Definition.Settings; - public ApplicationBase(IAppIndexerMapService appIndexerMapService, Logger logger) + public ApplicationBase(IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) { _appIndexerMapService = appIndexerMapService; + _indexerFactory = indexerFactory; _logger = logger; } @@ -62,5 +65,17 @@ namespace NzbDrone.Core.Applications { return null; } + + protected IndexerCapabilities GetIndexerCapabilities(IndexerDefinition indexer) + { + try + { + return _indexerFactory.GetInstance(indexer).GetCapabilities(); + } + catch (Exception) + { + return indexer.Capabilities; + } + } } } diff --git a/src/NzbDrone.Core/Applications/ApplicationFactory.cs b/src/NzbDrone.Core/Applications/ApplicationFactory.cs index 492710fee..2ecd2e78b 100644 --- a/src/NzbDrone.Core/Applications/ApplicationFactory.cs +++ b/src/NzbDrone.Core/Applications/ApplicationFactory.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Applications foreach (var application in applications) { - if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus)) + if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus) && blockedApplicationStatus.DisabledTill.HasValue) { _logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime()); continue; diff --git a/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs b/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs index 50318ecbd..769843dd3 100644 --- a/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs +++ b/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs @@ -13,6 +13,6 @@ namespace NzbDrone.Core.Applications public override bool SendUpdatesToClient => true; - public override string CompletionMessage => null; + public override string CompletionMessage => "Completed"; } } diff --git a/src/NzbDrone.Core/Applications/ApplicationService.cs b/src/NzbDrone.Core/Applications/ApplicationService.cs index 097b701d1..0a5a81fbd 100644 --- a/src/NzbDrone.Core/Applications/ApplicationService.cs +++ b/src/NzbDrone.Core/Applications/ApplicationService.cs @@ -127,6 +127,8 @@ namespace NzbDrone.Core.Applications private void SyncIndexers(List applications, List indexers, bool removeRemote = false, bool forceSync = false) { + var sortedIndexers = indexers.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList(); + foreach (var app in applications) { var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id); @@ -157,7 +159,7 @@ namespace NzbDrone.Core.Applications } } - foreach (var indexer in indexers) + foreach (var indexer in sortedIndexers) { var definition = indexer; @@ -252,11 +254,11 @@ namespace NzbDrone.Core.Applications if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) { - _logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message); + _logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message); } else { - _logger.Warn("{0} {1}", this, webException.Message); + _logger.Warn(webException, "{0} {1}", this, webException.Message); } } catch (TooManyRequestsException ex) @@ -264,12 +266,12 @@ namespace NzbDrone.Core.Applications var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1); _applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff); - _logger.Warn("API Request Limit reached for {0}", this); + _logger.Warn(ex, "API Request Limit reached for {0}", this); } catch (HttpException ex) { _applicationStatusService.RecordFailure(application.Definition.Id); - _logger.Warn("{0} {1}", this, ex.Message); + _logger.Warn(ex, "{0} {1}", this, ex.Message); } catch (Exception ex) { @@ -301,11 +303,11 @@ namespace NzbDrone.Core.Applications if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) { - _logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message); + _logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message); } else { - _logger.Warn("{0} {1}", this, webException.Message); + _logger.Warn(webException, "{0} {1}", this, webException.Message); } } catch (TooManyRequestsException ex) @@ -313,12 +315,12 @@ namespace NzbDrone.Core.Applications var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1); _applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff); - _logger.Warn("API Request Limit reached for {0}", this); + _logger.Warn(ex, "API Request Limit reached for {0}", this); } catch (HttpException ex) { _applicationStatusService.RecordFailure(application.Definition.Id); - _logger.Warn("{0} {1}", this, ex.Message); + _logger.Warn(ex, "{0} {1}", this, ex.Message); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs index 205bbe472..108972d6e 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _lazyLibrarianV1Proxy = lazyLibrarianV1Proxy; _configFileProvider = configFileProvider; @@ -65,7 +65,9 @@ namespace NzbDrone.Core.Applications.LazyLibrarian public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -74,7 +76,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol); + var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings); @@ -107,11 +109,12 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); var indexerProps = indexerMapping.RemoteIndexerName.Split(","); - var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol, indexerProps[1]); + var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerProps[1]); //Use the old remote id to find the indexer on LazyLibrarian incase the update was from a name change in Prowlarr var remoteIndexer = _lazyLibrarianV1Proxy.GetIndexer(indexerProps[1], lazyLibrarianIndexer.Type, Settings); @@ -133,7 +136,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to LazyLibrarian", indexer.Name, indexer.Id); var newRemoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings); @@ -146,7 +149,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian } } - private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null) + private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null) { var schema = protocol == DownloadProtocol.Usenet ? LazyLibrarianProviderType.Newznab : LazyLibrarianProviderType.Torznab; @@ -156,12 +159,19 @@ namespace NzbDrone.Core.Applications.LazyLibrarian Altername = $"{indexer.Name} (Prowlarr)", Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, - Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), + Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), Enabled = indexer.Enable, Type = schema, Priority = indexer.Priority }; + if (indexer.Protocol == DownloadProtocol.Torrent) + { + lazyLibrarianIndexer.MinimumSeeders = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders; + lazyLibrarianIndexer.SeedRatio = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio.GetValueOrDefault(); + lazyLibrarianIndexer.SeedTime = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime.GetValueOrDefault(); + } + return lazyLibrarianIndexer; } } diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs index bc20c3ddf..b13403aac 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs @@ -31,6 +31,9 @@ namespace NzbDrone.Core.Applications.LazyLibrarian public string Altername { get; set; } public LazyLibrarianProviderType Type { get; set; } public int Priority { get; set; } + public double SeedRatio { get; set; } + public int SeedTime { get; set; } + public int MinimumSeeders { get; set; } public bool Equals(LazyLibrarianIndexer other) { @@ -45,7 +48,10 @@ namespace NzbDrone.Core.Applications.LazyLibrarian other.Categories == Categories && other.Enabled == Enabled && other.Altername == Altername && - other.Priority == Priority; + other.Priority == Priority && + other.SeedRatio == SeedRatio && + other.SeedTime == SeedTime && + other.MinimumSeeders == MinimumSeeders; } } } diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs index 563f9ed3b..f4c6da138 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using FluentValidation.Results; @@ -96,6 +97,13 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { "dlpriority", CalculatePriority(indexer.Priority).ToString() } }; + if (indexer.Type == LazyLibrarianProviderType.Torznab) + { + parameters.Add("seeders", indexer.MinimumSeeders.ToString()); + parameters.Add("seed_ratio", indexer.SeedRatio.ToString(CultureInfo.InvariantCulture)); + parameters.Add("seed_duration", indexer.SeedTime.ToString()); + } + var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.Get, parameters); CheckForError(Execute(request)); return indexer; @@ -115,6 +123,13 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { "dlpriority", CalculatePriority(indexer.Priority).ToString() } }; + if (indexer.Type == LazyLibrarianProviderType.Torznab) + { + parameters.Add("seeders", indexer.MinimumSeeders.ToString()); + parameters.Add("seed_ratio", indexer.SeedRatio.ToString(CultureInfo.InvariantCulture)); + parameters.Add("seed_duration", indexer.SeedTime.ToString()); + } + var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.Get, parameters); CheckForError(Execute(request)); return indexer; diff --git a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs index 8a0615156..19c842f5c 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Lidarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _lidarrV1Proxy = lidarrV1Proxy; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Applications.Lidarr try { - failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); } catch (HttpException ex) { @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Lidarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Lidarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Lidarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Lidarr URL is invalid, Prowlarr cannot connect to Lidarr - are you missing a URL base?")); break; @@ -118,7 +119,9 @@ namespace NzbDrone.Core.Applications.Lidarr public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -127,7 +130,7 @@ namespace NzbDrone.Core.Applications.Lidarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol); + var lidarrIndexer = BuildLidarrIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings); @@ -159,10 +162,11 @@ namespace NzbDrone.Core.Applications.Lidarr { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var lidarrIndexer = BuildLidarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _lidarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -174,7 +178,7 @@ namespace NzbDrone.Core.Applications.Lidarr { _logger.Debug("Syncing remote indexer with current settings"); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { // Retain user fields not-affiliated with Prowlarr lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => lidarrIndexer.Fields.All(s => s.Name != f.Name))); @@ -200,7 +204,7 @@ namespace NzbDrone.Core.Applications.Lidarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Lidarr", indexer.Name, indexer.Id); lidarrIndexer.Id = 0; @@ -214,11 +218,11 @@ namespace NzbDrone.Core.Applications.Lidarr } } - private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0) + private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; if (id == 0) { @@ -250,7 +254,7 @@ namespace NzbDrone.Core.Applications.Lidarr lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; - lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { @@ -258,10 +262,15 @@ namespace NzbDrone.Core.Applications.Lidarr lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; - if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) + if (lidarrIndexer.Fields.Any(x => x.Name == "seedCriteria.discographySeedTime")) { lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; } + + if (lidarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")) + { + lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing; + } } return lidarrIndexer; diff --git a/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs index 45221126e..98c6125ff 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs @@ -55,6 +55,10 @@ namespace NzbDrone.Core.Applications.Lidarr var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var seedRatioCompare = seedRatio == otherSeedRatio; + var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing; + return other.EnableRss == EnableRss && other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableInteractiveSearch == EnableInteractiveSearch && @@ -62,7 +66,7 @@ namespace NzbDrone.Core.Applications.Lidarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare; + apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs index f53f3d90d..0197255a2 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs @@ -36,9 +36,12 @@ namespace NzbDrone.Core.Applications.Lidarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] public IEnumerable SyncCategories { get; set; } + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs index 9b7f37685..3fcb337c0 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs @@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Lidarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs index 245e38930..e9fd9ffe7 100644 --- a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs +++ b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.Applications.Mylar private readonly IMylarV3Proxy _mylarV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _mylarV3Proxy = mylarV3Proxy; _configFileProvider = configFileProvider; @@ -65,7 +65,9 @@ namespace NzbDrone.Core.Applications.Mylar public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -74,7 +76,7 @@ namespace NzbDrone.Core.Applications.Mylar _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol); + var mylarIndexer = BuildMylarIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings); @@ -107,11 +109,12 @@ namespace NzbDrone.Core.Applications.Mylar { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); var indexerProps = indexerMapping.RemoteIndexerName.Split(","); - var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol, indexerProps[1]); + var mylarIndexer = BuildMylarIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerProps[1]); //Use the old remote id to find the indexer on Mylar incase the update was from a name change in Prowlarr var remoteIndexer = _mylarV3Proxy.GetIndexer(indexerProps[1], mylarIndexer.Type, Settings); @@ -133,7 +136,7 @@ namespace NzbDrone.Core.Applications.Mylar { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Mylar", indexer.Name, indexer.Id); var newRemoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings); @@ -146,7 +149,7 @@ namespace NzbDrone.Core.Applications.Mylar } } - private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null) + private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null) { var schema = protocol == DownloadProtocol.Usenet ? MylarProviderType.Newznab : MylarProviderType.Torznab; @@ -156,7 +159,7 @@ namespace NzbDrone.Core.Applications.Mylar Altername = $"{indexer.Name} (Prowlarr)", Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, - Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), + Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), Enabled = indexer.Enable, Type = schema, }; diff --git a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs index cdb1ba5ac..85b9c4a3b 100644 --- a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs +++ b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Radarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _radarrV3Proxy = radarrV3Proxy; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Applications.Radarr try { - failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); } catch (HttpException ex) { @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Radarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Radarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Radarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Radarr URL is invalid, Prowlarr cannot connect to Radarr - are you missing a URL base?")); break; @@ -118,7 +119,9 @@ namespace NzbDrone.Core.Applications.Radarr public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -127,7 +130,7 @@ namespace NzbDrone.Core.Applications.Radarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol); + var radarrIndexer = BuildRadarrIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings); @@ -159,10 +162,11 @@ namespace NzbDrone.Core.Applications.Radarr { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var radarrIndexer = BuildRadarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _radarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -172,7 +176,7 @@ namespace NzbDrone.Core.Applications.Radarr if (!radarrIndexer.Equals(remoteIndexer) || forceSync) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { // Retain user fields not-affiliated with Prowlarr radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => radarrIndexer.Fields.All(s => s.Name != f.Name))); @@ -198,7 +202,7 @@ namespace NzbDrone.Core.Applications.Radarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Radarr", indexer.Name, indexer.Id); radarrIndexer.Id = 0; @@ -212,11 +216,11 @@ namespace NzbDrone.Core.Applications.Radarr } } - private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0) + private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; if (id == 0) { @@ -248,13 +252,18 @@ namespace NzbDrone.Core.Applications.Radarr radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; - radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; + + if (radarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")) + { + radarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing; + } } return radarrIndexer; diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs index d68dda0ad..3ae820f3a 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs @@ -51,6 +51,10 @@ namespace NzbDrone.Core.Applications.Radarr var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var seedRatioCompare = seedRatio == otherSeedRatio; + var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing; + return other.EnableRss == EnableRss && other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableInteractiveSearch == EnableInteractiveSearch && @@ -58,7 +62,7 @@ namespace NzbDrone.Core.Applications.Radarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare; + apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs index 3736b38f2..457d7d0df 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs @@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Radarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] public IEnumerable SyncCategories { get; set; } + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs index e74baad21..d431856aa 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs @@ -179,6 +179,7 @@ namespace NzbDrone.Core.Applications.Radarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs index e61a86c6f..1fc6742ae 100644 --- a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs +++ b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Readarr private readonly IReadarrV1Proxy _readarrV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _readarrV1Proxy = readarrV1Proxy; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Applications.Readarr try { - failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); } catch (HttpException ex) { @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Readarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Readarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Readarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Readarr URL is invalid, Prowlarr cannot connect to Readarr - are you missing a URL base?")); break; @@ -118,7 +119,9 @@ namespace NzbDrone.Core.Applications.Readarr public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -127,7 +130,7 @@ namespace NzbDrone.Core.Applications.Readarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol); + var readarrIndexer = BuildReadarrIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings); @@ -159,10 +162,11 @@ namespace NzbDrone.Core.Applications.Readarr { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var readarrIndexer = BuildReadarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _readarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -174,7 +178,7 @@ namespace NzbDrone.Core.Applications.Readarr { _logger.Debug("Syncing remote indexer with current settings"); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { // Retain user fields not-affiliated with Prowlarr readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => readarrIndexer.Fields.All(s => s.Name != f.Name))); @@ -200,7 +204,7 @@ namespace NzbDrone.Core.Applications.Readarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Readarr", indexer.Name, indexer.Id); readarrIndexer.Id = 0; @@ -214,11 +218,11 @@ namespace NzbDrone.Core.Applications.Readarr } } - private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0) + private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" }; + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -244,7 +248,7 @@ namespace NzbDrone.Core.Applications.Readarr readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; - readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { @@ -252,10 +256,15 @@ namespace NzbDrone.Core.Applications.Readarr readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; - if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) + if (readarrIndexer.Fields.Any(x => x.Name == "seedCriteria.discographySeedTime")) { readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; } + + if (readarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")) + { + readarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing; + } } return readarrIndexer; diff --git a/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs index a6c8ab7aa..b26b50ae0 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs @@ -55,6 +55,10 @@ namespace NzbDrone.Core.Applications.Readarr var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var seedRatioCompare = seedRatio == otherSeedRatio; + var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing; + return other.EnableRss == EnableRss && other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableInteractiveSearch == EnableInteractiveSearch && @@ -62,7 +66,7 @@ namespace NzbDrone.Core.Applications.Readarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare; + apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs index 55a7014bd..f789586d3 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs @@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Readarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] public IEnumerable SyncCategories { get; set; } + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs index 71e8a2c45..899ef79b6 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs @@ -153,6 +153,7 @@ namespace NzbDrone.Core.Applications.Readarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs index ec3db29ca..6e5284fc7 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Sonarr private readonly ISonarrV3Proxy _sonarrV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _sonarrV3Proxy = sonarrV3Proxy; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Applications.Sonarr try { - failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); } catch (HttpException ex) { @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Sonarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Sonarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Sonarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Sonarr URL is invalid, Prowlarr cannot connect to Sonarr - are you missing a URL base?")); break; @@ -122,8 +123,10 @@ namespace NzbDrone.Core.Applications.Sonarr public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() && - indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() && + indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -132,7 +135,7 @@ namespace NzbDrone.Core.Applications.Sonarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol); + var sonarrIndexer = BuildSonarrIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings); @@ -164,10 +167,11 @@ namespace NzbDrone.Core.Applications.Sonarr { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var sonarrIndexer = BuildSonarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _sonarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -179,7 +183,7 @@ namespace NzbDrone.Core.Applications.Sonarr { _logger.Debug("Syncing remote indexer with current settings"); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) { // Retain user fields not-affiliated with Prowlarr sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => sonarrIndexer.Fields.All(s => s.Name != f.Name))); @@ -206,7 +210,7 @@ namespace NzbDrone.Core.Applications.Sonarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Sonarr", indexer.Name, indexer.Id); sonarrIndexer.Id = 0; @@ -220,11 +224,11 @@ namespace NzbDrone.Core.Applications.Sonarr } } - private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0) + private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; if (id == 0) { @@ -256,8 +260,8 @@ namespace NzbDrone.Core.Applications.Sonarr sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; - sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); - sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray())); + sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray())); if (sonarrIndexer.Fields.Any(x => x.Name == "animeStandardFormatSearch")) { @@ -270,6 +274,11 @@ namespace NzbDrone.Core.Applications.Sonarr sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; + + if (sonarrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")) + { + sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing; + } } return sonarrIndexer; diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs index 8b0b6a561..698c7ed6f 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs @@ -61,6 +61,10 @@ namespace NzbDrone.Core.Applications.Sonarr var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var seedRatioCompare = seedRatio == otherSeedRatio; + var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing; + return other.EnableRss == EnableRss && other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableInteractiveSearch == EnableInteractiveSearch && @@ -68,7 +72,7 @@ namespace NzbDrone.Core.Applications.Sonarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && animeCats && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare; + apiKeyCompare && apiPathCompare && baseUrl && cats && animeCats && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs index 9c31df18c..95b52bab0 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs @@ -43,8 +43,11 @@ namespace NzbDrone.Core.Applications.Sonarr [FieldDefinition(4, Label = "Anime Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] public IEnumerable AnimeSyncCategories { get; set; } - [FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, Advanced = true, HelpText = "Sync also searching for anime using the standard numbering")] - public bool SyncAnimeStandardFormatSearch { get; set; } + [FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Sync also searching for anime using the standard numbering", Advanced = true)] + public bool SyncAnimeStandardFormatSearch { get; set; } = true; + + [FieldDefinition(6, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs index 48dcea40d..f92043c99 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs @@ -166,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs index 070cd5332..0c149fc7c 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Whisparr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) - : base(appIndexerMapService, logger) + public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _whisparrV3Proxy = whisparrV3Proxy; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Applications.Whisparr try { - failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); } catch (HttpException ex) { @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Applications.Whisparr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Whisparr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Whisparr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Whisparr URL is invalid, Prowlarr cannot connect to Whisparr - are you missing a URL base?")); break; @@ -118,7 +119,9 @@ namespace NzbDrone.Core.Applications.Whisparr public override void AddIndexer(IndexerDefinition indexer) { - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + var indexerCapabilities = GetIndexerCapabilities(indexer); + + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) { _logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id); @@ -127,7 +130,7 @@ namespace NzbDrone.Core.Applications.Whisparr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol); + var whisparrIndexer = BuildWhisparrIndexer(indexer, indexerCapabilities, indexer.Protocol); var remoteIndexer = _whisparrV3Proxy.AddIndexer(whisparrIndexer, Settings); @@ -159,10 +162,11 @@ namespace NzbDrone.Core.Applications.Whisparr { _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id); + var indexerCapabilities = GetIndexerCapabilities(indexer); var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var whisparrIndexer = BuildWhisparrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -174,7 +178,7 @@ namespace NzbDrone.Core.Applications.Whisparr { _logger.Debug("Syncing remote indexer with current settings"); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { // Retain user fields not-affiliated with Prowlarr whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => whisparrIndexer.Fields.All(s => s.Name != f.Name))); @@ -200,7 +204,7 @@ namespace NzbDrone.Core.Applications.Whisparr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Whisparr", indexer.Name, indexer.Id); whisparrIndexer.Id = 0; @@ -214,11 +218,11 @@ namespace NzbDrone.Core.Applications.Whisparr } } - private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0) + private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -244,13 +248,23 @@ namespace NzbDrone.Core.Applications.Whisparr whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; - whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; + + if (whisparrIndexer.Fields.Any(x => x.Name == "seedCriteria.seasonPackSeedTime")) + { + whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; + } + + if (whisparrIndexer.Fields.Any(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")) + { + whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value = Settings.SyncRejectBlocklistedTorrentHashesWhileGrabbing; + } } return whisparrIndexer; diff --git a/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs index 2edb460da..e8e6f8150 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs @@ -47,10 +47,18 @@ namespace NzbDrone.Core.Applications.Whisparr var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value); var seedTimeCompare = seedTime == otherSeedTime; + var seasonSeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value); + var otherSeasonSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value); + var seasonSeedTimeCompare = seasonSeedTime == otherSeasonSeedTime; + var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); var seedRatioCompare = seedRatio == otherSeedRatio; + var rejectBlocklistedTorrentHashesWhileGrabbing = Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var otherRejectBlocklistedTorrentHashesWhileGrabbing = other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "rejectBlocklistedTorrentHashesWhileGrabbing").Value); + var rejectBlocklistedTorrentHashesWhileGrabbingCompare = rejectBlocklistedTorrentHashesWhileGrabbing == otherRejectBlocklistedTorrentHashesWhileGrabbing; + return other.EnableRss == EnableRss && other.EnableAutomaticSearch == EnableAutomaticSearch && other.EnableInteractiveSearch == EnableInteractiveSearch && @@ -58,7 +66,7 @@ namespace NzbDrone.Core.Applications.Whisparr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare; + apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs index 5fe636747..0dfafc166 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs @@ -37,9 +37,12 @@ namespace NzbDrone.Core.Applications.Whisparr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] public IEnumerable SyncCategories { get; set; } + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "ApplicationSettingsSyncRejectBlocklistedTorrentHashes", HelpText = "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool SyncRejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs index 530005c5f..e2ee60524 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs @@ -151,6 +151,7 @@ namespace NzbDrone.Core.Applications.Whisparr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 14fc91efc..051d045bb 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -66,12 +66,19 @@ namespace NzbDrone.Core.Backup { _logger.ProgressInfo("Starting Backup"); + var backupFolder = GetBackupFolder(backupType); + _diskProvider.EnsureFolder(_backupTempFolder); - _diskProvider.EnsureFolder(GetBackupFolder(backupType)); + _diskProvider.EnsureFolder(backupFolder); + + if (!_diskProvider.FolderWritable(backupFolder)) + { + throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable"); + } var dateNow = DateTime.Now; var backupFilename = $"prowlarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; - var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); + var backupPath = Path.Combine(backupFolder, backupFilename); Cleanup(); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index a7b78fd92..f4715b203 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -10,6 +10,8 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Options; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Datastore; @@ -37,8 +39,10 @@ namespace NzbDrone.Core.Configuration bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } + ConsoleLogFormat ConsoleLogFormat { get; } bool LogSql { get; } int LogRotate { get; } + int LogSizeLimit { get; } bool FilterSentryEvents { get; } string Branch { get; } string ApiKey { get; } @@ -53,13 +57,15 @@ namespace NzbDrone.Core.Configuration string SyslogServer { get; } int SyslogPort { get; } string SyslogLevel { get; } + bool LogDbEnabled { get; } + string Theme { get; } string PostgresHost { get; } int PostgresPort { get; } string PostgresUser { get; } string PostgresPassword { get; } string PostgresMainDb { get; } string PostgresLogDb { get; } - string Theme { get; } + bool TrustCgnatIpAddresses { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -72,6 +78,11 @@ namespace NzbDrone.Core.Configuration private readonly IDiskProvider _diskProvider; private readonly ICached _cache; private readonly PostgresOptions _postgresOptions; + private readonly AuthOptions _authOptions; + private readonly AppOptions _appOptions; + private readonly ServerOptions _serverOptions; + private readonly UpdateOptions _updateOptions; + private readonly LogOptions _logOptions; private readonly string _configFile; private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -82,13 +93,23 @@ namespace NzbDrone.Core.Configuration ICacheManager cacheManager, IEventAggregator eventAggregator, IDiskProvider diskProvider, - IOptions postgresOptions) + IOptions postgresOptions, + IOptions authOptions, + IOptions appOptions, + IOptions serverOptions, + IOptions updateOptions, + IOptions logOptions) { _cache = cacheManager.GetCache(GetType()); _eventAggregator = eventAggregator; _diskProvider = diskProvider; _configFile = appFolderInfo.GetConfigPath(); _postgresOptions = postgresOptions.Value; + _authOptions = authOptions.Value; + _appOptions = appOptions.Value; + _serverOptions = serverOptions.Value; + _updateOptions = updateOptions.Value; + _logOptions = logOptions.Value; } public Dictionary GetConfigDictionary() @@ -144,7 +165,7 @@ namespace NzbDrone.Core.Configuration { const string defaultValue = "*"; - var bindAddress = GetValue("BindAddress", defaultValue); + var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue); if (string.IsNullOrWhiteSpace(bindAddress)) { return defaultValue; @@ -154,19 +175,19 @@ namespace NzbDrone.Core.Configuration } } - public int Port => GetValueInt("Port", DEFAULT_PORT); + public int Port => _serverOptions.Port ?? GetValueInt("Port", DEFAULT_PORT); - public int SslPort => GetValueInt("SslPort", DEFAULT_SSL_PORT); + public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", DEFAULT_SSL_PORT); - public bool EnableSsl => GetValueBoolean("EnableSsl", false); + public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false); - public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true); + public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true); public string ApiKey { get { - var apiKey = GetValue("ApiKey", GenerateApiKey()); + var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey()); if (apiKey.IsNullOrWhiteSpace()) { @@ -182,7 +203,7 @@ namespace NzbDrone.Core.Configuration { get { - var enabled = GetValueBoolean("AuthenticationEnabled", false, false); + var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false); if (enabled) { @@ -190,61 +211,92 @@ namespace NzbDrone.Core.Configuration return AuthenticationType.Basic; } - return GetValueEnum("AuthenticationMethod", AuthenticationType.None); + return Enum.TryParse(_authOptions.Method, out var enumValue) + ? enumValue + : GetValueEnum("AuthenticationMethod", AuthenticationType.None); } } - public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public AuthenticationRequiredType AuthenticationRequired => + Enum.TryParse(_authOptions.Required, out var enumValue) + ? enumValue + : GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); - public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); + public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false); - // TODO: Change back to "master" for the first stable release. - public string Branch => GetValue("Branch", "master").ToLowerInvariant(); + public string Branch => _updateOptions.Branch ?? GetValue("Branch", "master").ToLowerInvariant(); + + public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant(); + public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false); + + public ConsoleLogFormat ConsoleLogFormat => + Enum.TryParse(_logOptions.ConsoleFormat, out var enumValue) + ? enumValue + : GetValueEnum("ConsoleLogFormat", ConsoleLogFormat.Standard, false); + + public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false); - public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant(); - public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false); public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false); public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false); public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false); public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false); public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false); - public string Theme => GetValue("Theme", "auto", persist: false); - public bool LogSql => GetValueBoolean("LogSql", false, persist: false); - public int LogRotate => GetValueInt("LogRotate", 50, persist: false); - public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); - public string SslCertPath => GetValue("SslCertPath", ""); - public string SslCertPassword => GetValue("SslCertPassword", ""); + + public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false); + public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false); + public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false); + public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10); + public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false); + public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", ""); + public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", ""); public string UrlBase { get { - var urlBase = GetValue("UrlBase", "").Trim('/'); + var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/'); if (urlBase.IsNullOrWhiteSpace()) { return urlBase; } - return "/" + urlBase.Trim('/').ToLower(); + return "/" + urlBase; } } public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; - public string InstanceName => GetValue("InstanceName", BuildInfo.AppName); - public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false); + public string InstanceName + { + get + { + var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName); - public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); + if (instanceName.Contains(BuildInfo.AppName, StringComparison.OrdinalIgnoreCase)) + { + return instanceName; + } - public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false); + return BuildInfo.AppName; + } + } - public string SyslogServer => GetValue("SyslogServer", "", persist: false); + public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false); - public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false); + public UpdateMechanism UpdateMechanism => + Enum.TryParse(_updateOptions.Mechanism, out var enumValue) + ? enumValue + : GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); - public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant(); + public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false); + + public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false); + + public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false); + + public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant(); public int GetValueInt(string key, int defaultValue, bool persist = true) { @@ -277,13 +329,13 @@ namespace NzbDrone.Core.Configuration return valueHolder.First().Value.Trim(); } - //Save the value + // Save the value if (persist) { SetValue(key, defaultValue); } - //return the default value + // return the default value return defaultValue.ToString(); }); } @@ -332,8 +384,8 @@ namespace NzbDrone.Core.Configuration return; } - // If SSL is enabled and a cert hash is still in the config file disable SSL - if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace()) + // If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL + if (EnableSsl && (GetValue("SslCertHash", string.Empty, false).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace())) { SetValue("EnableSsl", false); } @@ -380,13 +432,21 @@ namespace NzbDrone.Core.Configuration throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Prowlarr will recreate it."); } - return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); + var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile)); + var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList(); + + if (config.Count != 1) + { + throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Prowlarr will recreate it."); + } + + return xDoc; } - var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); - xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); + var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); + newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); - return xDoc; + return newXDoc; } } catch (XmlException ex) @@ -420,5 +480,7 @@ namespace NzbDrone.Core.Configuration SetValue("ApiKey", GenerateApiKey()); _eventAggregator.PublishEvent(new ApiKeyChangedEvent()); } + + public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 208692c97..27a953823 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -183,6 +183,12 @@ namespace NzbDrone.Core.Configuration public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty); + public bool TrustCgnatIpAddresses + { + get { return GetValueBoolean("TrustCgnatIpAddresses", false); } + set { SetValue("TrustCgnatIpAddresses", value); } + } + private string GetValue(string key) { return GetValue(key, string.Empty); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index dc76a5a31..796e277b7 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -254,7 +254,7 @@ namespace NzbDrone.Core.Datastore protected void Delete(SqlBuilder builder) { - var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery(); + var sql = builder.AddDeleteTemplate(typeof(TModel)); using (var conn = _database.OpenConnection()) { diff --git a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs index ff97ec020..19c938737 100644 --- a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs +++ b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs @@ -9,8 +9,8 @@ namespace NzbDrone.Core.Datastore { public interface IConnectionStringFactory { - string MainDbConnectionString { get; } - string LogDbConnectionString { get; } + DatabaseConnectionInfo MainDbConnection { get; } + DatabaseConnectionInfo LogDbConnection { get; } string GetDatabasePath(string connectionString); } @@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore { _configFileProvider = configFileProvider; - MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : + MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : GetConnectionString(appFolderInfo.GetDatabase()); - LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : + LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : GetConnectionString(appFolderInfo.GetLogDatabase()); } - public string MainDbConnectionString { get; private set; } - public string LogDbConnectionString { get; private set; } + public DatabaseConnectionInfo MainDbConnection { get; private set; } + public DatabaseConnectionInfo LogDbConnection { get; private set; } public string GetDatabasePath(string connectionString) { @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Datastore return connectionBuilder.DataSource; } - private static string GetConnectionString(string dbPath) + private static DatabaseConnectionInfo GetConnectionString(string dbPath) { var connectionBuilder = new SQLiteConnectionStringBuilder { @@ -57,21 +57,22 @@ namespace NzbDrone.Core.Datastore connectionBuilder.Add("Full FSync", true); } - return connectionBuilder.ConnectionString; + return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString); } - private string GetPostgresConnectionString(string dbName) + private DatabaseConnectionInfo GetPostgresConnectionString(string dbName) { - var connectionBuilder = new NpgsqlConnectionStringBuilder(); + var connectionBuilder = new NpgsqlConnectionStringBuilder + { + Database = dbName, + Host = _configFileProvider.PostgresHost, + Username = _configFileProvider.PostgresUser, + Password = _configFileProvider.PostgresPassword, + Port = _configFileProvider.PostgresPort, + Enlist = false + }; - connectionBuilder.Database = dbName; - connectionBuilder.Host = _configFileProvider.PostgresHost; - connectionBuilder.Username = _configFileProvider.PostgresUser; - connectionBuilder.Password = _configFileProvider.PostgresPassword; - connectionBuilder.Port = _configFileProvider.PostgresPort; - connectionBuilder.Enlist = false; - - return connectionBuilder.ConnectionString; + return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs index 902a26009..fdcb227c6 100644 --- a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs @@ -2,18 +2,17 @@ using System; using System.Data; using Dapper; -namespace NzbDrone.Core.Datastore.Converters -{ - public class DapperTimeSpanConverter : SqlMapper.TypeHandler - { - public override void SetValue(IDbDataParameter parameter, TimeSpan value) - { - parameter.Value = value.ToString(); - } +namespace NzbDrone.Core.Datastore.Converters; - public override TimeSpan Parse(object value) - { - return TimeSpan.Parse((string)value); - } +public class TimeSpanConverter : SqlMapper.TypeHandler +{ + public override void SetValue(IDbDataParameter parameter, TimeSpan value) + { + parameter.Value = value.ToString(); + } + + public override TimeSpan Parse(object value) + { + return value is string str ? TimeSpan.Parse(str) : TimeSpan.Zero; } } diff --git a/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs b/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs index 82b4065f7..a8403187a 100644 --- a/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs +++ b/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Datastore } public CorruptDatabaseException(string message, Exception innerException, params object[] args) - : base(message, innerException, args) + : base(innerException, message, args) { } public CorruptDatabaseException(string message, Exception innerException) - : base(message, innerException) + : base(innerException, message) { } } diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index 887039bcb..741a22f0b 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -2,7 +2,6 @@ using System; using System.Data; using System.Data.Common; using System.Data.SQLite; -using System.Text.RegularExpressions; using Dapper; using NLog; using NzbDrone.Common.Instrumentation; @@ -52,9 +51,8 @@ namespace NzbDrone.Core.Datastore { using var db = _datamapperFactory(); var dbConnection = db as DbConnection; - var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", ""); - return new Version(version); + return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion); } } diff --git a/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs b/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs new file mode 100644 index 000000000..5b53f086f --- /dev/null +++ b/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Core.Datastore +{ + public class DatabaseConnectionInfo + { + public DatabaseConnectionInfo(DatabaseType databaseType, string connectionString) + { + DatabaseType = databaseType; + ConnectionString = connectionString; + } + + public DatabaseType DatabaseType { get; internal set; } + public string ConnectionString { get; internal set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs b/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs new file mode 100644 index 000000000..ffc77cf18 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs @@ -0,0 +1,16 @@ +using System; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Datastore; + +public static class DatabaseVersionParser +{ + private static readonly Regex VersionRegex = new (@"^[^ ]+", RegexOptions.Compiled); + + public static Version ParseServerVersion(string serverVersion) + { + var match = VersionRegex.Match(serverVersion); + + return match.Success ? new Version(match.Value) : null; + } +} diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index c592d17f2..0122757a7 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -2,6 +2,7 @@ using System; using System.Data.Common; using System.Data.SQLite; using System.Net.Sockets; +using System.Threading; using NLog; using Npgsql; using NzbDrone.Common.Disk; @@ -60,22 +61,22 @@ namespace NzbDrone.Core.Datastore public IDatabase Create(MigrationContext migrationContext) { - string connectionString; + DatabaseConnectionInfo connectionInfo; switch (migrationContext.MigrationType) { case MigrationType.Main: { - connectionString = _connectionStringFactory.MainDbConnectionString; - CreateMain(connectionString, migrationContext); + connectionInfo = _connectionStringFactory.MainDbConnection; + CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType); break; } case MigrationType.Log: { - connectionString = _connectionStringFactory.LogDbConnectionString; - CreateLog(connectionString, migrationContext); + connectionInfo = _connectionStringFactory.LogDbConnection; + CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType); break; } @@ -90,14 +91,14 @@ namespace NzbDrone.Core.Datastore { DbConnection conn; - if (connectionString.Contains(".db")) + if (connectionInfo.DatabaseType == DatabaseType.SQLite) { conn = SQLiteFactory.Instance.CreateConnection(); - conn.ConnectionString = connectionString; + conn.ConnectionString = connectionInfo.ConnectionString; } else { - conn = new NpgsqlConnection(connectionString); + conn = new NpgsqlConnection(connectionInfo.ConnectionString); } conn.Open(); @@ -107,12 +108,12 @@ namespace NzbDrone.Core.Datastore return db; } - private void CreateMain(string connectionString, MigrationContext migrationContext) + private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) { try { _restoreDatabaseService.Restore(); - _migrationController.Migrate(connectionString, migrationContext); + _migrationController.Migrate(connectionString, migrationContext, databaseType); } catch (SQLiteException e) { @@ -135,15 +136,17 @@ namespace NzbDrone.Core.Datastore { Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount); + Thread.Sleep(5000); + try { - _migrationController.Migrate(connectionString, migrationContext); + _migrationController.Migrate(connectionString, migrationContext, databaseType); + return; } catch (Exception ex) { if (--retryCount > 0) { - System.Threading.Thread.Sleep(5000); continue; } @@ -162,11 +165,11 @@ namespace NzbDrone.Core.Datastore } } - private void CreateLog(string connectionString, MigrationContext migrationContext) + private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) { try { - _migrationController.Migrate(connectionString, migrationContext); + _migrationController.Migrate(connectionString, migrationContext, databaseType); } catch (SQLiteException e) { @@ -186,7 +189,7 @@ namespace NzbDrone.Core.Datastore Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually."); } - _migrationController.Migrate(connectionString, migrationContext); + _migrationController.Migrate(connectionString, migrationContext, databaseType); } catch (Exception e) { diff --git a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs index c5e31f92c..67e251805 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs @@ -8,6 +8,12 @@ namespace NzbDrone.Core.Datastore.Extensions public static IContainer AddDatabase(this IContainer container) { container.RegisterDelegate(f => new MainDatabase(f.Create()), Reuse.Singleton); + + return container; + } + + public static IContainer AddLogDatabase(this IContainer container) + { container.RegisterDelegate(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton); return container; @@ -16,6 +22,12 @@ namespace NzbDrone.Core.Datastore.Extensions public static IContainer AddDummyDatabase(this IContainer container) { container.RegisterInstance(new MainDatabase(null)); + + return container; + } + + public static IContainer AddDummyLogDatabase(this IContainer container) + { container.RegisterInstance(new LogDatabase(null)); return container; diff --git a/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs b/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs new file mode 100644 index 000000000..93bfc0afc --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs @@ -0,0 +1,69 @@ +using System.Data; +using System.Text.RegularExpressions; +using FluentMigrator; +using NLog; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)] + public class DatabaseEngineVersionCheck : FluentMigrator.Migration + { + protected readonly Logger _logger; + + public DatabaseEngineVersionCheck() + { + _logger = NzbDroneLogger.GetLogger(this); + } + + public override void Up() + { + IfDatabase("sqlite").Execute.WithConnection(LogSqliteVersion); + IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion); + } + + public override void Down() + { + // No-op + } + + private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran) + { + using (var versionCmd = conn.CreateCommand()) + { + versionCmd.Transaction = tran; + versionCmd.CommandText = "SELECT sqlite_version();"; + + using (var reader = versionCmd.ExecuteReader()) + { + while (reader.Read()) + { + var version = reader.GetString(0); + + _logger.Info("SQLite {0}", version); + } + } + } + } + + private void LogPostgresVersion(IDbConnection conn, IDbTransaction tran) + { + using (var versionCmd = conn.CreateCommand()) + { + versionCmd.Transaction = tran; + versionCmd.CommandText = "SHOW server_version"; + + using (var reader = versionCmd.ExecuteReader()) + { + while (reader.Read()) + { + var version = reader.GetString(0); + var cleanVersion = Regex.Replace(version, @"\(.*?\)", ""); + + _logger.Info("Postgres {0}", cleanVersion); + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs b/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs new file mode 100644 index 000000000..f275bbd70 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(039)] + public class email_encryption : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeEncryption); + } + + private void ChangeEncryption(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + using (var getEmailCmd = conn.CreateCommand()) + { + getEmailCmd.Transaction = tran; + getEmailCmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'"; + + using (var reader = getEmailCmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + settings["useEncryption"] = settings.Value("requireEncryption") ?? false ? 1 : 0; + settings["requireEncryption"] = null; + + updated.Add(new + { + Settings = settings.ToJson(), + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/040_newznab_category_to_capabilities_settings.cs b/src/NzbDrone.Core/Datastore/Migration/040_newznab_category_to_capabilities_settings.cs new file mode 100644 index 000000000..ad573bd9c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/040_newznab_category_to_capabilities_settings.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(40)] + public class newznab_category_to_capabilities_settings : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(MoveCategoriesToCapabilities); + } + + private void MoveCategoriesToCapabilities(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" IN ('Newznab', 'Torznab')"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + if ((settings.Value("capabilities")?.ContainsKey("categories") ?? false) == false + && settings.ContainsKey("categories") + && settings.TryGetValue("categories", out var categories)) + { + if (!settings.ContainsKey("capabilities")) + { + settings.Add("capabilities", new JObject()); + } + + settings.Value("capabilities")?.Add(new JProperty("categories", JArray.FromObject(categories))); + + if (settings.ContainsKey("categories")) + { + settings.Remove("categories"); + } + } + + updated.Add(new + { + Settings = settings.ToJson(), + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/041_gazelle_freeleech_token_options.cs b/src/NzbDrone.Core/Datastore/Migration/041_gazelle_freeleech_token_options.cs new file mode 100644 index 000000000..56a11c732 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/041_gazelle_freeleech_token_options.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(041)] + public class gazelle_freeleech_token_options : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(MigrateIndexersToTokenOptions); + } + + private void MigrateIndexersToTokenOptions(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" IN ('Orpheus', 'Redacted', 'AlphaRatio', 'BrokenStones', 'CGPeers', 'DICMusic', 'GreatPosterWall', 'SecretCinema')"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + if (settings.ContainsKey("useFreeleechToken") && settings.Value("useFreeleechToken").Type == JTokenType.Boolean) + { + var optionValue = settings.Value("useFreeleechToken") switch + { + true => 2, // Required + _ => 0 // Never + }; + + settings.Remove("useFreeleechToken"); + settings.Add("useFreeleechToken", optionValue); + } + + updated.Add(new + { + Id = id, + Settings = settings.ToJson() + }); + } + } + } + + var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/042_myanonamouse_freeleech_wedge_options.cs b/src/NzbDrone.Core/Datastore/Migration/042_myanonamouse_freeleech_wedge_options.cs new file mode 100644 index 000000000..5a93488d5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/042_myanonamouse_freeleech_wedge_options.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(042)] + public class myanonamouse_freeleech_wedge_options : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(MigrateIndexersToWedgeOptions); + } + + private void MigrateIndexersToWedgeOptions(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'MyAnonamouse'"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + if (settings.ContainsKey("freeleech") && settings.Value("freeleech").Type == JTokenType.Boolean) + { + var optionValue = settings.Value("freeleech") switch + { + true => 2, // Required + _ => 0 // Never + }; + + settings.Remove("freeleech"); + settings.Add("useFreeleechWedge", optionValue); + } + + updated.Add(new + { + Id = id, + Settings = settings.ToJson() + }); + } + } + } + + var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index 45593cb53..74151de7d 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework { public interface IMigrationController { - void Migrate(string connectionString, MigrationContext migrationContext); + void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType); } public class MigrationController : IMigrationController @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework _migrationLoggerProvider = migrationLoggerProvider; } - public void Migrate(string connectionString, MigrationContext migrationContext) + public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) { var sw = Stopwatch.StartNew(); @@ -37,17 +37,18 @@ namespace NzbDrone.Core.Datastore.Migration.Framework ServiceProvider serviceProvider; - var db = connectionString.Contains(".db") ? "sqlite" : "postgres"; + var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres"; serviceProvider = new ServiceCollection() .AddLogging(b => b.AddNLog()) .AddFluentMigratorCore() + .Configure(cfg => cfg.IncludeUntaggedMaintenances = true) .ConfigureRunner( builder => builder .AddPostgres() .AddNzbDroneSQLite() .WithGlobalConnectionString(connectionString) - .WithMigrationsIn(Assembly.GetExecutingAssembly())) + .ScanIn(Assembly.GetExecutingAssembly()).For.All()) .Configure(opt => opt.Namespace = "NzbDrone.Core.Datastore.Migration") .Configure(opt => { diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs index d0a661751..a56384466 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs @@ -219,7 +219,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework protected virtual IList ReadTables() { - const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"; + const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name;"; var dtTable = Read(sqlCommand).Tables[0]; var tableDefinitionList = new List(); diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 546d1288b..637f61b99 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -109,7 +109,6 @@ namespace NzbDrone.Core.Datastore SqlMapper.RemoveTypeMap(typeof(DateTime)); SqlMapper.AddTypeHandler(new DapperUtcConverter()); - SqlMapper.AddTypeHandler(new DapperTimeSpanConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new CookieConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); @@ -123,6 +122,9 @@ namespace NzbDrone.Core.Datastore SqlMapper.RemoveTypeMap(typeof(Guid)); SqlMapper.RemoveTypeMap(typeof(Guid?)); SqlMapper.AddTypeHandler(new GuidConverter()); + SqlMapper.RemoveTypeMap(typeof(TimeSpan)); + SqlMapper.RemoveTypeMap(typeof(TimeSpan?)); + SqlMapper.AddTypeHandler(new TimeSpanConverter()); SqlMapper.AddTypeHandler(new CommandConverter()); SqlMapper.AddTypeHandler(new SystemVersionConverter()); } diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index ae076c14b..3085dbf63 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2 ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs index 9cc2d9793..74f653f76 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Download.Extensions; @@ -95,8 +96,14 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddUri(Aria2Settings settings, string magnet) { - var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List { magnet }); + var options = new Dictionary(); + if (settings.Directory.IsNotNullOrWhiteSpace()) + { + options.Add("dir", settings.Directory); + } + + var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List { magnet }, options); var gid = response.GetStringResponse(); return gid; @@ -104,8 +111,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddTorrent(Aria2Settings settings, byte[] torrent) { - var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent); + // Aria2's second parameter is an array of URIs and needs to be sent if options are provided, this satisfies that requirement. + var emptyListOfUris = new List(); + var options = new Dictionary(); + if (settings.Directory.IsNotNullOrWhiteSpace()) + { + options.Add("dir", settings.Directory); + } + + var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent, emptyListOfUris, options); var gid = response.GetStringResponse(); return gid; diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs index e88bc4cc1..f90ea6306 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs @@ -32,15 +32,18 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [FieldDefinition(1, Label = "Port", Type = FieldType.Number)] public int Port { get; set; } - [FieldDefinition(2, Label = "XML RPC Path", Type = FieldType.Textbox)] + [FieldDefinition(2, Label = "XmlRpcPath", Type = FieldType.Textbox)] public string RpcPath { get; set; } - [FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } - [FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] + [FieldDefinition(4, Label = "SecretToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string SecretToken { get; set; } + [FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")] + public string Directory { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index f8fd44e97..9e22a7460 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.Blackhole @@ -20,8 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs index a25d0fd4d..81793f947 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs @@ -27,15 +27,16 @@ namespace NzbDrone.Core.Download.Clients.Blackhole private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .torrent file")] + [FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] + [FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")] public string TorrentFolder { get; set; } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [FieldDefinition(1, Label = "Save Magnet Files", Type = FieldType.Checkbox, HelpText = "Save a .magnet file with the magnet link if no .torrent file is available (only useful if the download client supports .magnet files)")] + [FieldDefinition(1, Label = "TorrentBlackholeSaveMagnetFiles", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesHelpText")] public bool SaveMagnetFiles { get; set; } - [FieldDefinition(2, Label = "Save Magnet Files", Type = FieldType.Textbox, HelpText = "Extension to use for magnet links, defaults to '.magnet'")] + [FieldDefinition(2, Label = "TorrentBlackholeSaveMagnetFilesExtension", Type = FieldType.Textbox, HelpText = "TorrentBlackholeSaveMagnetFilesExtensionHelpText")] public string MagnetFileExtension { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 0f0364e61..0b64150ee 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.Blackhole @@ -16,8 +17,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole public UsenetBlackhole(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, logger) + : base(httpClient, configService, diskProvider, localizationService, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs index cb53385e5..d4b011d47 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs @@ -18,7 +18,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .nzb file")] + [FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] + [FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")] public string NzbFolder { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index ca63ba0e7..90bd6ba1f 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -23,8 +24,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index bf463eb81..64ff55a10 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -34,22 +34,24 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Deluge")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Deluge")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/json")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback Category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(5, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index bc3e8ca1c..fea6d8fc6 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -36,7 +36,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Download Station")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Download Station")] public bool UseSsl { get; set; } [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -45,10 +46,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + [FieldDefinition(5, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")] + [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientDownloadStationSettingsDirectoryHelpText")] public string TvDirectory { get; set; } public DownloadStationSettings() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 17cf57246..97afe9480 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -33,8 +34,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _dsInfoProxy = dsInfoProxy; _dsTaskProxySelector = dsTaskProxySelector; @@ -308,14 +310,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - else if (Settings.Category.IsNotNullOrWhiteSpace()) - { - var destDir = GetDefaultDir(); + var destDir = GetDefaultDir(); + + if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace()) + { return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } - return null; + return destDir.TrimEnd('/'); } protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 562d6d975..e78f5f5d2 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -31,8 +32,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, logger) + : base(httpClient, configService, diskProvider, localizationService, logger) { _dsInfoProxy = dsInfoProxy; _dsTaskProxySelector = dsTaskProxySelector; @@ -274,14 +276,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - else if (Settings.Category.IsNotNullOrWhiteSpace()) - { - var destDir = GetDefaultDir(); + var destDir = GetDefaultDir(); + + if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace()) + { return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } - return null; + return destDir.TrimEnd('/'); } protected override string AddFromLink(ReleaseInfo release) diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index 73c6de8a2..5d39d7b5f 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -4,9 +4,11 @@ using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Flood.Models; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -21,8 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Flood ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } @@ -56,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Flood } } - return result; + return result.Where(t => t.IsNotNullOrWhiteSpace()); } public override string Name => "Flood"; diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs index 46f6f19cb..1026e93e9 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs @@ -40,10 +40,12 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Flood")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Flood")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, HelpText = "Optionally adds a prefix to Flood API, such as [protocol]://[host]:[port]/[urlBase]api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, HelpText = "DownloadClientFloodSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "[protocol]://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -52,16 +54,16 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, HelpText = "Manually specifies download destination")] + [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDestinationHelpText")] public string Destination { get; set; } - [FieldDefinition(7, Label = "Tags", Type = FieldType.Tag, HelpText = "Initial tags of a download. To be recognized, a download must have all initial tags. This avoids conflicts with unrelated downloads.")] + [FieldDefinition(7, Label = "Tags", Type = FieldType.Tag, HelpText = "DownloadClientFloodSettingsTagsHelpText")] public IEnumerable Tags { get; set; } - [FieldDefinition(8, Label = "Additional Tags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "Adds properties of media as tags. Hints are examples.", Advanced = true)] + [FieldDefinition(8, Label = "DownloadClientFloodSettingsAdditionalTags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "DownloadClientFloodSettingsAdditionalTagsHelpText", Advanced = true)] public IEnumerable AdditionalTags { get; set; } - [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs index ee0fff8e1..effdf37d9 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs @@ -46,34 +46,39 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload ApiUrl = "/api/v1/"; } - [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")] + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsHostHelpText")] + [FieldToken(TokenField.HelpText, "Host", "url", "mafreebox.freebox.fr")] public string Host { get; set; } - [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")] + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsPortHelpText")] + [FieldToken(TokenField.HelpText, "Port", "port", 443)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Freebox API")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")] + [FieldDefinition(3, Label = "DownloadClientFreeboxSettingsApiUrl", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientFreeboxSettingsApiUrlHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientFreeboxSettingsApiUrl", "url", "http://[host]:[port]/[api_base_url]/[api_version]/")] + [FieldToken(TokenField.HelpText, "DownloadClientFreeboxSettingsApiUrl", "defaultApiUrl", "/api/v1/")] public string ApiUrl { get; set; } - [FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")] + [FieldDefinition(4, Label = "DownloadClientFreeboxSettingsAppId", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsAppIdHelpText")] public string AppId { get; set; } - [FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")] + [FieldDefinition(5, Label = "DownloadClientFreeboxSettingsAppToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "DownloadClientFreeboxSettingsAppTokenHelpText")] public string AppToken { get; set; } - [FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")] + [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsDestinationHelpText")] public string DestinationDirectory { get; set; } - [FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated non-Prowlarr downloads (will create a [category] subdirectory in the output directory)")] + [FieldDefinition(7, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs index cec808592..00e7e06b4 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.FreeboxDownload @@ -19,8 +20,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index b5aa1eb06..560c40eb3 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -20,8 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index c8e6f7763..c26923afb 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -39,10 +39,13 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Hadouken")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Hadouken")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Hadouken")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -51,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release.")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index e1088780d..a87460d71 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -20,8 +21,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, logger) + : base(httpClient, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 2dae1ac49..1f276fa25 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -41,16 +41,18 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBVortex url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(2, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "NZBVortex")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } - [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(4, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index b286bf922..49ada68e6 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -18,15 +19,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public class Nzbget : UsenetClientBase { private readonly INzbgetProxy _proxy; - private readonly string[] _successStatus = { "SUCCESS", "NONE" }; - private readonly string[] _deleteFailedStatus = { "HEALTH", "DUPE", "SCAN", "COPY", "BAD" }; public Nzbget(INzbgetProxy proxy, IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, logger) + : base(httpClient, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 0e1cbc912..7f968b512 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -41,10 +41,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "NZBGet")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBGet url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "NZBGet")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/jsonrpc")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -53,13 +56,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority for items added from Prowlarr")] + [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NZBGet version 16.0")] + [FieldDefinition(8, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 1e98734f7..99f80ab22 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -17,8 +18,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { public Pneumatic(IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, logger) + : base(configService, diskProvider, localizationService, logger) { } @@ -39,9 +41,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); - var nzbData = await indexer.Download(url); + var downloadResponse = await indexer.Download(url); - File.WriteAllBytes(nzbFile, nzbData); + await File.WriteAllBytesAsync(nzbFile, downloadResponse.Data); _logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs index 741021a3f..6cd8a2b89 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -19,10 +19,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); - [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "This folder will need to be reachable from XBMC")] + [FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")] public string NzbFolder { get; set; } - [FieldDefinition(1, Label = "Strm Folder", Type = FieldType.Path, HelpText = ".strm files in this folder will be import by drone")] + [FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")] public string StrmFolder { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 6555ce6cc..3d1863784 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -30,8 +31,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent IConfigService configService, IDiskProvider diskProvider, ICacheManager cacheManager, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxySelector = proxySelector; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs new file mode 100644 index 000000000..874fbff7a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Download.Clients.QBittorrent +{ + public enum QBittorrentContentLayout + { + Default = 0, + Original = 1, + Subfolder = 2 + } +} diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 14bee89fc..33fcfc5ca 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -25,8 +25,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Dictionary GetLabels(QBittorrentSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); - void PauseTorrent(string hash, QBittorrentSettings settings); - void ResumeTorrent(string hash, QBittorrentSettings settings); void SetForceStart(string hash, bool enabled, QBittorrentSettings settings); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index cdf161c7f..7c9cc8768 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -146,7 +146,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) { request.AddFormParameter("paused", true); } @@ -176,7 +176,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) { request.AddFormParameter("paused", true); } @@ -212,7 +212,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) { var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel") .Post() @@ -254,7 +254,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden) { return; } @@ -263,22 +263,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void PauseTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/command/pause") - .Post() - .AddFormParameter("hash", hash); - ProcessRequest(request, settings); - } - - public void ResumeTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/command/resume") - .Post() - .AddFormParameter("hash", hash); - ProcessRequest(request, settings); - } - public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/command/setForceStart") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index 5e967d095..cfa7c9934 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -246,14 +246,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", category); } - // Note: ForceStart is handled by separate api call - if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + // Avoid extraneous API version check if initial state is ForceStart + if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop) { - request.AddFormParameter("paused", false); - } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) - { - request.AddFormParameter("paused", true); + var stoppedParameterName = GetApiVersion(settings) >= new Version(2, 11, 0) ? "stopped" : "paused"; + + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter(stoppedParameterName, false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) + { + request.AddFormParameter(stoppedParameterName, true); + } } if (settings.SequentialOrder) @@ -265,6 +271,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("firstLastPiecePrio", true); } + + if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original) + { + request.AddFormParameter("contentLayout", "Original"); + } + else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder) + { + request.AddFormParameter("contentLayout", "Subfolder"); + } } public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) @@ -282,7 +297,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0 - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) { return; } @@ -304,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled - if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict) + if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict) { return; } @@ -313,22 +328,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void PauseTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/api/v2/torrents/pause") - .Post() - .AddFormParameter("hashes", hash); - ProcessRequest(request, settings); - } - - public void ResumeTorrent(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/api/v2/torrents/resume") - .Post() - .AddFormParameter("hashes", hash); - ProcessRequest(request, settings); - } - public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 4427472ae..8d157dc30 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -35,10 +35,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsUseSslHelpText")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "qBittorrent")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -47,21 +49,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] + [FieldDefinition(8, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "DownloadClientQbittorrentSettingsInitialStateHelpText")] public int InitialState { get; set; } - [FieldDefinition(9, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")] + [FieldDefinition(9, Label = "DownloadClientQbittorrentSettingsSequentialOrder", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsSequentialOrderHelpText")] public bool SequentialOrder { get; set; } - [FieldDefinition(10, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")] + [FieldDefinition(10, Label = "DownloadClientQbittorrentSettingsFirstAndLastFirst", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText")] public bool FirstAndLast { get; set; } + [FieldDefinition(11, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] + public int ContentLayout { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs index 56c5ddf1a..b8fddbc11 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs @@ -1,9 +1,16 @@ +using NzbDrone.Core.Annotations; + namespace NzbDrone.Core.Download.Clients.QBittorrent { public enum QBittorrentState { + [FieldOption(Label = "Started")] Start = 0, + + [FieldOption(Label = "Force Started")] ForceStart = 1, - Pause = 2 + + [FieldOption(Label = "Stopped")] + Stop = 2 } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 584e392b2..246262527 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -22,8 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, logger) + : base(httpClient, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index 61b5f9228..e25a91701 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; using NzbDrone.Common.Disk; using NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters; @@ -7,10 +7,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { public class SabnzbdConfig { + public SabnzbdConfig() + { + Categories = new List(); + Servers = new List(); + } + public SabnzbdConfigMisc Misc { get; set; } - public List Categories { get; set; } - public List Servers { get; set; } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 37df94bd5..70dba78b1 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -50,13 +50,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Sabnzbd")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Sabnzbd")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } - [FieldDefinition(4, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(4, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } [FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -65,10 +68,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(7, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs index c62a1f6ff..ad14de894 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -5,6 +5,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Transmission { @@ -15,8 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index f5cf1fed9..977e3bfee 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -20,8 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 2c2334f44..76f8684e0 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -4,6 +4,7 @@ using System.Net; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -208,7 +209,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) { - var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); + var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}"; var sessionId = _authSessionIDCache.Find(authKey); @@ -220,24 +221,26 @@ namespace NzbDrone.Core.Download.Clients.Transmission authLoginRequest.SuppressHttpError = true; var response = _httpClient.Execute(authLoginRequest); - if (response.StatusCode == HttpStatusCode.MovedPermanently) - { - var url = response.Headers.GetSingleValue("Location"); - throw new DownloadClientException("Remote site redirected to " + url); - } - else if (response.StatusCode == HttpStatusCode.Conflict) + switch (response.StatusCode) { - sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); + case HttpStatusCode.MovedPermanently: + var url = response.Headers.GetSingleValue("Location"); - if (sessionId == null) - { - throw new DownloadClientException("Remote host did not return a Session Id."); - } - } - else - { - throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); + throw new DownloadClientException("Remote site redirected to " + url); + case HttpStatusCode.Forbidden: + throw new DownloadClientException($"Failed to authenticate with Transmission. It may be necessary to add {BuildInfo.AppName}'s IP address to RPC whitelist."); + case HttpStatusCode.Conflict: + sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id"); + + if (sessionId == null) + { + throw new DownloadClientException("Remote host did not return a Session Id."); + } + + break; + default: + throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); } _logger.Debug("Transmission authentication succeeded."); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 4d2682ee6..2a510b5e0 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -41,10 +41,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Transmission")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Transmission")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/rpc")] + [FieldToken(TokenField.HelpText, "UrlBase", "defaultUrl", "/transmission/")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -53,16 +57,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategorySubFolderHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")] + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] public string Directory { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index 79f5df0e4..3b87962bb 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Transmission; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Vuze { @@ -16,8 +17,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 5218f9ebe..628ebdf52 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.rTorrent; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent IConfigService configService, IDiskProvider diskProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index e2ab37fe2..8e3cd6827 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -36,10 +36,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to ruTorrent")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "rTorrent")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. When using ruTorrent this usually is RPC2 or (path to ruTorrent)/plugins/rpc/rpc.php")] + [FieldDefinition(3, Label = "DownloadClientRTorrentSettingsUrlPath", Type = FieldType.Textbox, HelpText = "DownloadClientRTorrentSettingsUrlPathHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientRTorrentSettingsUrlPath", "url", "http(s)://[host]:[port]/[urlPath]")] + [FieldToken(TokenField.HelpText, "DownloadClientRTorrentSettingsUrlPath", "url2", "/plugins/rpc/rpc.php")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -48,16 +51,16 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional.")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientRTorrentSettingsDirectoryHelpText")] public string Directory { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(9, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to ruTorrent in a stopped state")] + [FieldDefinition(9, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")] public bool AddStopped { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index ef12042fb..98ad41eec 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -7,7 +7,9 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.UTorrent @@ -21,8 +23,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) { _proxy = proxy; } @@ -72,6 +75,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent } public override string Name => "uTorrent"; + + public override ProviderMessage Message => new (_localizationService.GetLocalizedString("DownloadClientUTorrentProviderMessage"), ProviderMessageType.Warning); + public override bool SupportsCategories => true; protected override void Test(List failures) diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index d3b72c04d..573fa2f1d 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -34,10 +34,13 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to uTorrent")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "uTorrent")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "uTorrent")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -46,13 +49,14 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release. Adding a category specific to Prowlarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing items")] + [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] + [FieldDefinition(8, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "DownloadClientSettingsInitialStateHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")] public int IntialState { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 7659c32a6..9a7da6fd6 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Download { protected readonly IConfigService _configService; protected readonly IDiskProvider _diskProvider; + protected readonly ILocalizationService _localizationService; protected readonly Logger _logger; public abstract string Name { get; } @@ -40,10 +42,12 @@ namespace NzbDrone.Core.Download protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) { _configService = configService; _diskProvider = diskProvider; + _localizationService = localizationService; _logger = logger; } diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index e7eacbbe3..19aedf751 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download foreach (var client in clients) { - if (blockedClients.TryGetValue(client.Definition.Id, out var downloadClientStatus)) + if (blockedClients.TryGetValue(client.Definition.Id, out var downloadClientStatus) && downloadClientStatus.DisabledTill.HasValue) { _logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime()); continue; diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 675ab3b8e..223c24384 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -1,10 +1,8 @@ using System; using System.Threading.Tasks; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Common.TPL; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; @@ -27,7 +25,6 @@ namespace NzbDrone.Core.Download private readonly IDownloadClientStatusService _downloadClientStatusService; private readonly IIndexerFactory _indexerFactory; private readonly IIndexerStatusService _indexerStatusService; - private readonly IRateLimitService _rateLimitService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -35,7 +32,6 @@ namespace NzbDrone.Core.Download IDownloadClientStatusService downloadClientStatusService, IIndexerFactory indexerFactory, IIndexerStatusService indexerStatusService, - IRateLimitService rateLimitService, IEventAggregator eventAggregator, Logger logger) { @@ -43,7 +39,6 @@ namespace NzbDrone.Core.Download _downloadClientStatusService = downloadClientStatusService; _indexerFactory = indexerFactory; _indexerStatusService = indexerStatusService; - _rateLimitService = rateLimitService; _eventAggregator = eventAggregator; _logger = logger; } @@ -132,15 +127,7 @@ namespace NzbDrone.Core.Download _logger.Trace("Attempting download of {0}", link); var url = new Uri(link); - // Limit grabs to 2 per second. - if (link.IsNotNullOrWhiteSpace() && !link.StartsWith("magnet:")) - { - await _rateLimitService.WaitAndPulseAsync(url.Host, TimeSpan.FromSeconds(2)); - } - var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId)); - var success = false; - var downloadedBytes = Array.Empty(); var release = new ReleaseInfo { @@ -151,17 +138,21 @@ namespace NzbDrone.Core.Download DownloadProtocol = indexer.Protocol }; - var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl) + var grabEvent = new IndexerDownloadEvent(release, false, source, host, release.Title, release.DownloadUrl) { Indexer = indexer, GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api }; + byte[] downloadedBytes; + try { - downloadedBytes = await indexer.Download(url); + var downloadResponse = await indexer.Download(url); + downloadedBytes = downloadResponse.Data; _indexerStatusService.RecordSuccess(indexerId); grabEvent.Successful = true; + grabEvent.ElapsedTime = downloadResponse.ElapsedTime; } catch (ReleaseUnavailableException) { diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index d9d7f7133..217e70a31 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, logger) + : base(configService, diskProvider, localizationService, logger) { _torrentFileInfoReader = torrentFileInfoReader; _seedConfigProvider = seedConfigProvider; @@ -127,9 +129,8 @@ namespace NzbDrone.Core.Download private async Task DownloadFromWebUrl(TorrentInfo release, IIndexer indexer, string torrentUrl) { - byte[] torrentFile = null; - - torrentFile = await indexer.Download(new Uri(torrentUrl)); + var downloadResponse = await indexer.Download(new Uri(torrentUrl)); + var torrentFile = downloadResponse.Data; // handle magnet URLs if (torrentFile.Length >= 7 @@ -171,9 +172,7 @@ namespace NzbDrone.Core.Download } catch (FormatException ex) { - _logger.Error(ex, "Failed to parse magnetlink for release '{0}': '{1}'", release.Title, magnetUrl); - - return null; + throw new ReleaseDownloadException("Failed to parse magnetlink for release '{0}': '{1}'", ex, release.Title, magnetUrl); } if (hash != null) diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index 2541c6059..1e85ffbb9 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -5,6 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -19,8 +20,9 @@ namespace NzbDrone.Core.Download protected UsenetClientBase(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, + ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, logger) + : base(configService, diskProvider, localizationService, logger) { _httpClient = httpClient; } @@ -41,12 +43,10 @@ namespace NzbDrone.Core.Download var filename = StringUtil.CleanFileName(release.Title) + ".nzb"; - byte[] nzbData; - - nzbData = await indexer.Download(url); + var downloadResponse = await indexer.Download(url); _logger.Info("Adding report [{0}] to the queue.", release.Title); - return AddFromNzbFile(release, filename, nzbData); + return AddFromNzbFile(release, filename, downloadResponse.Data); } } } diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 37a775e96..483f52b6b 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -20,26 +20,6 @@ namespace NzbDrone.Core return actual; } - public static long Megabytes(this int megabytes) - { - return Convert.ToInt64(megabytes * 1024L * 1024L); - } - - public static long Gigabytes(this int gigabytes) - { - return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); - } - - public static long Megabytes(this double megabytes) - { - return Convert.ToInt64(megabytes * 1024L * 1024L); - } - - public static long Gigabytes(this double gigabytes) - { - return Convert.ToInt64(gigabytes * 1024L * 1024L * 1024L); - } - public static long Round(this long number, long level) { return Convert.ToInt64(Math.Floor((decimal)number / level) * level); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs index e65fe0972..d3198b111 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs @@ -1,4 +1,5 @@ -using NLog; +using System.Collections.Generic; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Lifecycle; @@ -28,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { _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, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), MinimumLength), "#invalid-api-key"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary { { "length", MinimumLength } }), "#invalid-api-key"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs index cef8b239e..6be6f572d 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; @@ -39,10 +40,19 @@ namespace NzbDrone.Core.HealthCheck.Checks if (backOffProviders.Count == enabledProviders.Count) { - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#download-clients-are-unavailable-due-to-failures"); + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString("DownloadClientStatusAllClientHealthCheckMessage"), + "#download-clients-are-unavailable-due-to-failures"); } - return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("DownloadClientStatusCheckSingleClientMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures"); + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + _localizationService.GetLocalizedString("DownloadClientStatusSingleClientHealthCheckMessage", new Dictionary + { + { "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), + "#download-clients-are-unavailable-due-to-failures"); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs index 0d1682efc..592304606 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; @@ -39,7 +40,10 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())), + _localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) } + }), "#invalid-indexer-download-client-setting"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs index 758259a13..f65f44962 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -17,9 +18,7 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IIndexerFactory _providerFactory; private readonly IIndexerStatusService _providerStatusService; - public IndexerLongTermStatusCheck(IIndexerFactory providerFactory, - IIndexerStatusService providerStatusService, - ILocalizationService localizationService) + public IndexerLongTermStatusCheck(IIndexerFactory providerFactory, IIndexerStatusService providerStatusService, ILocalizationService localizationService) : base(localizationService) { _providerFactory = providerFactory; @@ -46,14 +45,16 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"), + _localizationService.GetLocalizedString("IndexerLongTermStatusAllUnavailableHealthCheckMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"), - string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#indexers-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs similarity index 53% rename from src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs rename to src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs index 73d217c25..d6f0ad90c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Definitions.Cardigann; @@ -9,12 +10,12 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderDeletedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] - public class NoDefinitionCheck : HealthCheckBase + public class IndexerNoDefinitionCheck : HealthCheckBase { private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService; private readonly IIndexerFactory _indexerFactory; - public NoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService) + public IndexerNoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService) : base(localizationService) { _indexerDefinitionUpdateService = indexerDefinitionUpdateService; @@ -23,23 +24,22 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var currentDefs = _indexerDefinitionUpdateService.All(); + var currentDefinitions = _indexerDefinitionUpdateService.All(); + var noDefinitionIndexers = _indexerFactory.AllProviders(false) + .Where(i => i.Definition.Implementation == "Cardigann" && currentDefinitions.All(d => d.File != ((CardigannSettings)i.Definition.Settings).DefinitionFile)) + .ToList(); - var noDefIndexers = _indexerFactory.AllProviders(false) - .Where(i => i.Definition.Implementation == "Cardigann" && currentDefs.All(d => d.File != ((CardigannSettings)i.Definition.Settings).DefinitionFile)).ToList(); - - if (noDefIndexers.Count == 0) + if (noDefinitionIndexers.Count == 0) { return new HealthCheck(GetType()); } - var healthType = HealthCheckResult.Error; - var healthMessage = string.Format(_localizationService.GetLocalizedString("IndexerNoDefCheckMessage"), - string.Join(", ", noDefIndexers.Select(v => v.Definition.Name))); - return new HealthCheck(GetType(), - healthType, - healthMessage, + HealthCheckResult.Error, + _localizationService.GetLocalizedString("IndexerNoDefinitionCheckHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", noDefinitionIndexers.Select(v => v.Definition.Name).ToArray()) } + }), "#indexers-have-no-definition"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs similarity index 69% rename from src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs rename to src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs index 484f2bc19..39aeac49e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerProxies; @@ -9,11 +10,11 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderDeletedEvent))] [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] - public class IndexerProxyCheck : HealthCheckBase + public class IndexerProxyStatusCheck : HealthCheckBase { private readonly IIndexerProxyFactory _proxyFactory; - public IndexerProxyCheck(IIndexerProxyFactory proxyFactory, + public IndexerProxyStatusCheck(IIndexerProxyFactory proxyFactory, ILocalizationService localizationService) : base(localizationService) { @@ -37,15 +38,17 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerProxyStatusCheckAllClientMessage"), - "#proxies-are-unavailable-due-to-failures"); + _localizationService.GetLocalizedString("IndexerProxyStatusAllUnavailableHealthCheckMessage"), + "#indexer-proxies-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerProxyStatusCheckSingleClientMessage"), - string.Join(", ", badProxies.Select(v => v.Definition.Name))), - "#proxies-are-unavailable-due-to-failures"); + _localizationService.GetLocalizedString("IndexerProxyStatusUnavailableHealthCheckMessage", new Dictionary + { + { "indexerProxyNames", string.Join(", ", badProxies.Select(v => v.Definition.Name)) } + }), + "#indexer-proxies-are-unavailable-due-to-failures"); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs index 968cb5e8f..2e8846a75 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -44,14 +45,16 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"), + _localizationService.GetLocalizedString("IndexerStatusAllUnavailableHealthCheckMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"), - string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#indexers-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs index 4265b5963..2337403b8 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] public class IndexerVIPCheck : HealthCheckBase { @@ -24,7 +25,7 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var indexers = _indexerFactory.AllProviders(false); + var indexers = _indexerFactory.Enabled(false); var expiringProviders = new List(); foreach (var provider in indexers) @@ -39,12 +40,8 @@ namespace NzbDrone.Core.HealthCheck.Checks var expiration = (string)vipProp.GetValue(provider.Definition.Settings); - if (expiration.IsNullOrWhiteSpace()) - { - continue; - } - - if (DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7))) + if (expiration.IsNotNullOrWhiteSpace() && + DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7))) { expiringProviders.Add(provider); } @@ -53,10 +50,12 @@ namespace NzbDrone.Core.HealthCheck.Checks if (!expiringProviders.Empty()) { return new HealthCheck(GetType(), - HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiringClientMessage"), - string.Join(", ", expiringProviders.Select(v => v.Definition.Name))), - "#indexer-vip-expiring"); + HealthCheckResult.Warning, + _localizationService.GetLocalizedString("IndexerVipExpiringHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", expiringProviders.Select(v => v.Definition.Name).ToArray()) } + }), + "#indexer-vip-expiring"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs index 8b0dd06e7..0f3dffc1e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] public class IndexerVIPExpiredCheck : HealthCheckBase { @@ -24,7 +25,7 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var indexers = _indexerFactory.AllProviders(false); + var indexers = _indexerFactory.Enabled(false); var expiredProviders = new List(); foreach (var provider in indexers) @@ -39,12 +40,8 @@ namespace NzbDrone.Core.HealthCheck.Checks var expiration = (string)vipProp.GetValue(provider.Definition.Settings); - if (expiration.IsNullOrWhiteSpace()) - { - continue; - } - - if (DateTime.Parse(expiration).Before(DateTime.Now)) + if (expiration.IsNotNullOrWhiteSpace() && + DateTime.Parse(expiration).Before(DateTime.Now)) { expiredProviders.Add(provider); } @@ -53,10 +50,12 @@ namespace NzbDrone.Core.HealthCheck.Checks if (!expiredProviders.Empty()) { return new HealthCheck(GetType(), - HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"), - string.Join(", ", expiredProviders.Select(v => v.Definition.Name))), - "#indexer-vip-expired"); + HealthCheckResult.Error, + _localizationService.GetLocalizedString("IndexerVipExpiredHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", expiredProviders.Select(v => v.Definition.Name).ToArray()) } + }), + "#indexer-vip-expired"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs index c9b5e2561..daf5ee725 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; @@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary + { + { "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#notifications-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs index f8f7df340..ba1e40657 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using NLog; @@ -19,7 +20,7 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IHttpRequestBuilderFactory _cloudRequestBuilder; - public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, ILocalizationService localizationService, Logger logger) + public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, Logger logger, ILocalizationService localizationService) : base(localizationService) { _configService = configService; @@ -31,35 +32,58 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - if (_configService.ProxyEnabled) + if (!_configService.ProxyEnabled) { - var addresses = Dns.GetHostAddresses(_configService.ProxyHostname); - if (!addresses.Any()) - { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname)); - } + return new HealthCheck(GetType()); + } - var request = _cloudRequestBuilder.Create() - .Resource("/ping") - .Build(); + var addresses = Dns.GetHostAddresses(_configService.ProxyHostname); - try - { - var response = _client.Execute(request); - - // We only care about 400 responses, other error codes can be ignored - if (response.StatusCode == HttpStatusCode.BadRequest) + if (!addresses.Any()) + { + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage", new Dictionary { - _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode)); - } - } - catch (Exception ex) + { "proxyHostName", _configService.ProxyHostname } + }), + "#proxy-failed-resolve-ip"); + } + + var request = _cloudRequestBuilder.Create() + .Resource("/ping") + .Build(); + + try + { + var response = _client.Execute(request); + + // We only care about 400 responses, other error codes can be ignored + if (response.StatusCode == HttpStatusCode.BadRequest) { - _logger.Error(ex, "Proxy Health Check failed"); - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url)); + _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); + + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString("ProxyBadRequestHealthCheckMessage", new Dictionary + { + { "statusCode", response.StatusCode } + }), + "#proxy-failed-test"); } } + catch (Exception ex) + { + _logger.Error(ex, "Proxy Health Check failed"); + + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage", new Dictionary + { + { "url", request.Url } + }), + "#proxy-failed-test"); + } return new HealthCheck(GetType()); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs index 8ee1f5a30..d2b3acf13 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IHttpRequestBuilderFactory _cloudRequestBuilder; private readonly Logger _logger; - public SystemTimeCheck(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, ILocalizationService localizationService, Logger logger) + public SystemTimeCheck(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, Logger logger, ILocalizationService localizationService) : base(localizationService) { _client = client; @@ -29,19 +29,26 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - var request = _cloudRequestBuilder.Create() - .Resource("/time") - .Build(); - - var response = _client.Execute(request); - var result = Json.Deserialize(response.Content); - var systemTime = DateTime.UtcNow; - - // +/- more than 1 day - if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1) + try { - _logger.Error("System time mismatch. SystemTime: {0} Expected Time: {1}. Update system time", systemTime, result.DateTimeUtc); - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("SystemTimeCheckMessage")); + var request = _cloudRequestBuilder.Create() + .Resource("/time") + .Build(); + + var response = _client.Execute(request); + var result = Json.Deserialize(response.Content); + var systemTime = DateTime.UtcNow; + + // +/- more than 1 day + 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"); + } + } + catch (Exception e) + { + _logger.Warn(e, "Unable to verify system time"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index f92e29b80..684d7f60a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -39,7 +40,7 @@ namespace NzbDrone.Core.HealthCheck.Checks var startupFolder = _appFolderInfo.StartUpFolder; var uiFolder = Path.Combine(startupFolder, "UI"); - if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) && + if (_configFileProvider.UpdateAutomatically && _configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && !_osInfo.IsDocker) { @@ -47,7 +48,12 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupTranslocationMessage"), startupFolder), + _localizationService.GetLocalizedString( + "UpdateStartupTranslocationHealthCheckMessage", + new Dictionary + { + { "startupFolder", startupFolder } + }), "#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder."); } @@ -55,7 +61,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupNotWritableMessage"), startupFolder, Environment.UserName), + _localizationService.GetLocalizedString( + "UpdateStartupNotWritableHealthCheckMessage", + new Dictionary + { + { "startupFolder", startupFolder }, + { "userName", Environment.UserName } + }), "#cannot-install-update-because-startup-folder-is-not-writable-by-the-user"); } @@ -63,14 +75,31 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateCheckUINotWritableMessage"), uiFolder, Environment.UserName), + _localizationService.GetLocalizedString( + "UpdateUiNotWritableHealthCheckMessage", + new Dictionary + { + { "uiFolder", uiFolder }, + { "userName", Environment.UserName } + }), "#cannot-install-update-because-ui-folder-is-not-writable-by-the-user"); } } - if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null) + if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14)) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new-update-is-available"); + var latestAvailable = _checkUpdateService.AvailableUpdate(); + + if (latestAvailable != null) + { + return new HealthCheck(GetType(), + BuildInfo.BuildDateTime.Before(DateTime.UtcNow.AddDays(-180)) ? HealthCheckResult.Error : HealthCheckResult.Warning, + _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary + { + { "version", $"v{latestAvailable.Version}" } + }), + "#new-update-is-available"); + } } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index a5ee00372..e78e5d229 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; @@ -203,9 +204,29 @@ namespace NzbDrone.Core.History history.Data.Add("Host", message.Host ?? string.Empty); history.Data.Add("GrabMethod", message.Redirect ? "Redirect" : "Proxy"); history.Data.Add("GrabTitle", message.Title); - history.Data.Add("Categories", string.Join(",", message.Release.Categories.Select(x => x.Id) ?? Array.Empty())); history.Data.Add("Url", message.Url ?? string.Empty); + if (message.ElapsedTime > 0) + { + history.Data.Add("ElapsedTime", message.ElapsedTime.ToString()); + } + + if (message.Release.InfoUrl.IsNotNullOrWhiteSpace()) + { + history.Data.Add("InfoUrl", message.Release.InfoUrl); + } + + if (message.DownloadClient.IsNotNullOrWhiteSpace() || message.DownloadClientName.IsNotNullOrWhiteSpace()) + { + history.Data.Add("DownloadClient", message.DownloadClient ?? string.Empty); + history.Data.Add("DownloadClientName", message.DownloadClientName ?? string.Empty); + } + + if (message.Release.PublishDate != DateTime.MinValue) + { + history.Data.Add("PublishedDate", message.Release.PublishDate.ToUniversalTime().ToString("s") + "Z"); + } + _historyRepository.Insert(history); } @@ -219,7 +240,7 @@ namespace NzbDrone.Core.History Successful = message.Successful }; - history.Data.Add("ElapsedTime", message.Time.ToString()); + history.Data.Add("ElapsedTime", message.ElapsedTime.ToString()); _historyRepository.Insert(history); } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs index a719652af..5763a563e 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs @@ -1,18 +1,26 @@ -using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Instrumentation; namespace NzbDrone.Core.Housekeeping.Housekeepers { public class TrimLogDatabase : IHousekeepingTask { private readonly ILogRepository _logRepo; + private readonly IConfigFileProvider _configFileProvider; - public TrimLogDatabase(ILogRepository logRepo) + public TrimLogDatabase(ILogRepository logRepo, IConfigFileProvider configFileProvider) { _logRepo = logRepo; + _configFileProvider = configFileProvider; } public void Clean() { + if (!_configFileProvider.LogDbEnabled) + { + return; + } + _logRepo.Trim(); } } diff --git a/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs b/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs index a56bcf2e6..24b5aa67f 100644 --- a/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs +++ b/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Net; +using NetTools; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration; @@ -52,7 +54,15 @@ namespace NzbDrone.Core.Http //We are utilizing the WebProxy implementation here to save us having to re-implement it. This way we use Microsofts implementation var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray); - return proxy.IsBypassed((Uri)url); + return proxy.IsBypassed((Uri)url) || IsBypassedByIpAddressRange(proxySettings.BypassListAsArray, url.Host); + } + + private static bool IsBypassedByIpAddressRange(string[] bypassList, string host) + { + return bypassList.Any(bypass => + IPAddressRange.TryParse(bypass, out var ipAddressRange) && + IPAddress.TryParse(host, out var ipAddress) && + ipAddressRange.Contains(ipAddress)); } } } diff --git a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs index c3edb880e..5107bf151 100644 --- a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs +++ b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs @@ -102,9 +102,15 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr var url = request.Url.ToString(); var maxTimeout = Settings.RequestTimeout * 1000; - // Use Proxy if no credentials are set (creds not supported as of FS 2.2.9) var proxySettings = _proxySettingsProvider.GetProxySettings(); - var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null; + var proxyUrl = proxySettings != null ? GetProxyUri(proxySettings) : null; + + var requestProxy = new FlareSolverrProxy + { + Url = proxyUrl?.OriginalString, + Username = proxySettings != null && proxySettings.Username.IsNotNullOrWhiteSpace() ? proxySettings.Username : null, + Password = proxySettings != null && proxySettings.Password.IsNotNullOrWhiteSpace() ? proxySettings.Password : null + }; if (request.Method == HttpMethod.Get) { @@ -113,10 +119,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr Cmd = "request.get", Url = url, MaxTimeout = maxTimeout, - Proxy = new FlareSolverrProxy - { - Url = proxyUrl?.AbsoluteUri - } + Proxy = requestProxy }; } else if (request.Method == HttpMethod.Post) @@ -139,10 +142,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr ContentLength = null }, MaxTimeout = maxTimeout, - Proxy = new FlareSolverrProxy - { - Url = proxyUrl?.AbsoluteUri - } + Proxy = requestProxy }; } else if (contentTypeType.Contains("multipart/form-data") @@ -169,6 +169,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr newRequest.LogResponseContent = true; newRequest.RequestTimeout = TimeSpan.FromSeconds(Settings.RequestTimeout + 5); newRequest.SetContent(req.ToJson()); + newRequest.ContentSummary = req.ToJson(Formatting.None); _logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url); @@ -189,16 +190,16 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr if (response.StatusCode != HttpStatusCode.OK) { - _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); - failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode))); + _logger.Error("Proxy validation failed: {0}", response.StatusCode); + failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary { { "statusCode", response.StatusCode } }))); } var result = JsonConvert.DeserializeObject(response.Content); } catch (Exception ex) { - _logger.Error(ex, "Proxy Health Check failed"); - failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url.Host))); + _logger.Error(ex, "Proxy validation failed"); + failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } }))); } return new ValidationResult(failures); @@ -206,17 +207,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr private Uri GetProxyUri(HttpProxySettings proxySettings) { - switch (proxySettings.Type) + return proxySettings.Type switch { - case ProxyType.Http: - return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port); - case ProxyType.Socks4: - return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port); - case ProxyType.Socks5: - return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port); - default: - return null; - } + ProxyType.Http => new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port), + ProxyType.Socks4 => new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port), + ProxyType.Socks5 => new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port), + _ => null + }; } private class FlareSolverrRequest @@ -247,6 +244,8 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr private class FlareSolverrProxy { public string Url { get; set; } + public string Username { get; set; } + public string Password { get; set; } } private class HeadersPost diff --git a/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs index b82251d6e..d7c9acc23 100644 --- a/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs +++ b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs @@ -41,14 +41,14 @@ namespace NzbDrone.Core.IndexerProxies // We only care about 400 responses, other error codes can be ignored if (response.StatusCode == HttpStatusCode.BadRequest) { - _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); - failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy. StatusCode: {0}", response.StatusCode))); + _logger.Error("Proxy validation failed: {0}", response.StatusCode); + failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary { { "statusCode", response.StatusCode } }))); } } catch (Exception ex) { - _logger.Error(ex, "Proxy Health Check failed"); - failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message))); + _logger.Error(ex, "Proxy validation failed"); + failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } }))); } return new ValidationResult(failures); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs index a79878a98..d4c13c863 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs @@ -31,9 +31,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions !IsIdSearch; public override bool IsIdSearch => - Episode.IsNotNullOrWhiteSpace() || ImdbId.IsNotNullOrWhiteSpace() || - Season.HasValue || TvdbId.HasValue || RId.HasValue || TraktId.HasValue || @@ -116,7 +114,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions string episodeString; if (DateTime.TryParseExact($"{Season} {Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) { - episodeString = showDate.ToString("yyyy.MM.dd"); + episodeString = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture); } else if (Episode.IsNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs b/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs index e41d758d4..6b398da77 100644 --- a/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs +++ b/src/NzbDrone.Core/IndexerSearch/NewznabResults.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.IndexerSearch return new XElement(feedNamespace + "attr", new XAttribute("name", name), new XAttribute("value", value)); } - public string ToXml(DownloadProtocol protocol) + public string ToXml(DownloadProtocol protocol, bool preferMagnetUrl = false) { // IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode // characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly @@ -73,6 +73,7 @@ namespace NzbDrone.Core.IndexerSearch new XElement("title", "Prowlarr"), from r in Releases let t = (r as TorrentInfo) ?? new TorrentInfo() + let downloadUrl = preferMagnetUrl ? t.MagnetUrl ?? r.DownloadUrl : r.DownloadUrl ?? t.MagnetUrl select new XElement("item", new XElement("title", RemoveInvalidXMLChars(r.Title)), new XElement("description", RemoveInvalidXMLChars(r.Description)), @@ -85,11 +86,11 @@ namespace NzbDrone.Core.IndexerSearch r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl), r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)), new XElement("size", r.Size), - new XElement("link", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty), + new XElement("link", downloadUrl ?? string.Empty), r.Categories == null ? null : from c in r.Categories select new XElement("category", c.Id), new XElement( "enclosure", - new XAttribute("url", r.DownloadUrl ?? t.MagnetUrl ?? string.Empty), + new XAttribute("url", downloadUrl ?? string.Empty), r.Size == null ? null : new XAttribute("length", r.Size), new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")), r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol), diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 03f546c42..4d5fbd5d3 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -177,8 +177,8 @@ namespace NzbDrone.Core.IndexerSearch if (criteriaBase.Categories is { Length: > 0 }) { - //Only query supported indexers - indexers = indexers.Where(i => ((IndexerDefinition)i.Definition).Capabilities.Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList(); + // Only query supported indexers + indexers = indexers.Where(i => i.GetCapabilities().Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList(); if (indexers.Count == 0) { @@ -196,7 +196,7 @@ namespace NzbDrone.Core.IndexerSearch var reports = batch.SelectMany(x => x).ToList(); - _logger.Debug("Total of {0} reports were found for {1} from {2} indexer(s)", reports.Count, criteriaBase, indexers.Count); + _logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexer(s)", reports.Count, criteriaBase, indexers.Count); return reports; } @@ -217,7 +217,7 @@ namespace NzbDrone.Core.IndexerSearch //Filter results to only those in searched categories if (criteriaBase.Categories.Length > 0) { - var expandedQueryCats = ((IndexerDefinition)indexer.Definition).Capabilities.Categories.ExpandTorznabQueryCategories(criteriaBase.Categories); + var expandedQueryCats = indexer.GetCapabilities().Categories.ExpandTorznabQueryCategories(criteriaBase.Categories); releases = releases.Where(result => result.Categories?.Any() != true || expandedQueryCats.Intersect(result.Categories.Select(c => c.Id)).Any()).ToList(); diff --git a/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs b/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs index 261d0052a..76cbfd616 100644 --- a/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs +++ b/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.IndexerStats public int IndexerId { get; set; } public string IndexerName { get; set; } public int AverageResponseTime { get; set; } + public int AverageGrabResponseTime { get; set; } public int NumberOfQueries { get; set; } public int NumberOfGrabs { get; set; } public int NumberOfRssQueries { get; set; } diff --git a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs index fd603ece8..bfaa6e508 100644 --- a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs +++ b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs @@ -26,11 +26,15 @@ namespace NzbDrone.Core.IndexerStats { var history = _historyService.Between(start, end); - var filteredHistory = history.Where(h => indexerIds.Contains(h.IndexerId)); + var filteredHistory = history.Where(h => indexerIds.Contains(h.IndexerId)).ToArray(); - var groupedByIndexer = filteredHistory.GroupBy(h => h.IndexerId); - var groupedByUserAgent = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("source") ?? ""); - var groupedByHost = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("host") ?? ""); + var groupedByIndexer = filteredHistory.GroupBy(h => h.IndexerId).ToArray(); + var groupedByUserAgent = filteredHistory + .Where(h => h.EventType != HistoryEventType.IndexerAuth) + .GroupBy(h => h.Data.GetValueOrDefault("source") ?? "").ToArray(); + var groupedByHost = filteredHistory + .Where(h => h.EventType != HistoryEventType.IndexerAuth) + .GroupBy(h => h.Data.GetValueOrDefault("host") ?? "").ToArray(); var indexerStatsList = new List(); var userAgentStatsList = new List(); @@ -57,17 +61,13 @@ namespace NzbDrone.Core.IndexerStats .ThenBy(v => v.Id) .ToArray(); - var temp = 0; - var elapsedTimeEvents = sortedEvents - .Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp) && h.Data.GetValueOrDefault("cached") != "1") - .Select(h => temp) - .ToArray(); - - indexerStats.AverageResponseTime = elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0; + indexerStats.AverageResponseTime = CalculateAverageElapsedTime(sortedEvents.Where(h => h.EventType is HistoryEventType.IndexerRss or HistoryEventType.IndexerQuery).ToArray()); + indexerStats.AverageGrabResponseTime = CalculateAverageElapsedTime(sortedEvents.Where(h => h.EventType is HistoryEventType.ReleaseGrabbed).ToArray()); foreach (var historyEvent in sortedEvents) { var failed = !historyEvent.Successful; + switch (historyEvent.EventType) { case HistoryEventType.IndexerQuery: @@ -101,8 +101,6 @@ namespace NzbDrone.Core.IndexerStats indexerStats.NumberOfFailedRssQueries++; } - break; - default: break; } } @@ -118,8 +116,8 @@ namespace NzbDrone.Core.IndexerStats }; var sortedEvents = indexer.OrderBy(v => v.Date) - .ThenBy(v => v.Id) - .ToArray(); + .ThenBy(v => v.Id) + .ToArray(); foreach (var historyEvent in sortedEvents) { @@ -128,13 +126,10 @@ namespace NzbDrone.Core.IndexerStats case HistoryEventType.IndexerRss: case HistoryEventType.IndexerQuery: indexerStats.NumberOfQueries++; - break; case HistoryEventType.ReleaseGrabbed: indexerStats.NumberOfGrabs++; break; - default: - break; } } @@ -149,8 +144,8 @@ namespace NzbDrone.Core.IndexerStats }; var sortedEvents = indexer.OrderBy(v => v.Date) - .ThenBy(v => v.Id) - .ToArray(); + .ThenBy(v => v.Id) + .ToArray(); foreach (var historyEvent in sortedEvents) { @@ -163,8 +158,6 @@ namespace NzbDrone.Core.IndexerStats case HistoryEventType.ReleaseGrabbed: indexerStats.NumberOfGrabs++; break; - default: - break; } } @@ -178,5 +171,17 @@ namespace NzbDrone.Core.IndexerStats HostStatistics = hostStatsList }; } + + private static int CalculateAverageElapsedTime(History.History[] sortedEvents) + { + var temp = 0; + + var elapsedTimeEvents = sortedEvents + .Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp) && temp > 0 && h.Data.GetValueOrDefault("cached") != "1") + .Select(_ => temp) + .ToArray(); + + return elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0; + } } } diff --git a/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs b/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs index 6b2c4dc91..bf886d02f 100644 --- a/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs +++ b/src/NzbDrone.Core/IndexerVersions/IndexerDefinitionUpdateService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Net; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; @@ -29,7 +30,7 @@ namespace NzbDrone.Core.IndexerVersions /* Update Service will fall back if version # does not exist for an indexer per Ta */ private const string DEFINITION_BRANCH = "master"; - private const int DEFINITION_VERSION = 9; + private const int DEFINITION_VERSION = 11; // Used when moving yml to C# private readonly List _definitionBlocklist = new () @@ -46,6 +47,8 @@ namespace NzbDrone.Core.IndexerVersions "desitorrents", "hdbits", "lat-team", + "mteamtp", + "mteamtp2fa", "reelflix", "shareisland", "skipthecommercials", @@ -92,8 +95,10 @@ namespace NzbDrone.Core.IndexerVersions var response = _httpClient.Get>(request); indexerList = response.Resource.Where(i => !_definitionBlocklist.Contains(i.File)).ToList(); } - catch + catch (Exception ex) { + _logger.Warn(ex, "Error while getting indexer definitions, fallback to reading from disk."); + var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions"); indexerList = ReadDefinitionsFromDisk(indexerList, definitionFolder); @@ -104,9 +109,9 @@ namespace NzbDrone.Core.IndexerVersions indexerList = ReadDefinitionsFromDisk(indexerList, customDefinitionFolder); } - catch + catch (Exception ex) { - _logger.Error("Failed to Connect to Indexer Definition Server for Indexer listing"); + _logger.Error(ex, "Failed to Connect to Indexer Definition Server for Indexer listing"); } return indexerList; @@ -114,7 +119,7 @@ namespace NzbDrone.Core.IndexerVersions public CardigannDefinition GetCachedDefinition(string fileKey) { - if (string.IsNullOrEmpty(fileKey)) + if (string.IsNullOrWhiteSpace(fileKey)) { throw new ArgumentNullException(nameof(fileKey)); } @@ -137,11 +142,11 @@ namespace NzbDrone.Core.IndexerVersions if (directoryInfo.Exists) { - var files = directoryInfo.GetFiles($"*.yml", options); + var files = directoryInfo.GetFiles("*.yml", options); foreach (var file in files) { - _logger.Debug("Loading definition " + file.FullName); + _logger.Debug("Loading definition {0}", file.FullName); try { @@ -158,9 +163,9 @@ namespace NzbDrone.Core.IndexerVersions indexerList.Add(definition); } - catch (Exception e) + catch (Exception ex) { - _logger.Error($"Error while parsing Cardigann definition {file.FullName}\n{e}"); + _logger.Error(ex, "Error while parsing Cardigann definition {0}", file.FullName); } } } @@ -170,7 +175,7 @@ namespace NzbDrone.Core.IndexerVersions private CardigannDefinition GetUncachedDefinition(string fileKey) { - if (string.IsNullOrEmpty(fileKey)) + if (string.IsNullOrWhiteSpace(fileKey)) { throw new ArgumentNullException(nameof(fileKey)); } @@ -188,7 +193,8 @@ namespace NzbDrone.Core.IndexerVersions if (files.Any()) { var file = files.First(); - _logger.Trace("Loading Cardigann definition " + file.FullName); + _logger.Trace("Loading Cardigann definition {0}", file.FullName); + try { var definitionString = File.ReadAllText(file.FullName); @@ -196,9 +202,9 @@ namespace NzbDrone.Core.IndexerVersions return CleanIndexerDefinition(definition); } - catch (Exception e) + catch (Exception ex) { - _logger.Error($"Error while parsing Cardigann definition {file.FullName}\n{e}"); + _logger.Error(ex, "Error while parsing Cardigann definition {0}", file.FullName); } } } @@ -206,7 +212,7 @@ namespace NzbDrone.Core.IndexerVersions var dbDefs = _versionService.All(); //Check to ensure it's in versioned defs before we go to web - if (dbDefs.Count > 0 && !dbDefs.Any(x => x.File == fileKey)) + if (dbDefs.Count > 0 && dbDefs.All(x => x.File != fileKey)) { throw new ArgumentNullException(nameof(fileKey)); } @@ -217,9 +223,25 @@ namespace NzbDrone.Core.IndexerVersions private CardigannDefinition GetHttpDefinition(string id) { - var req = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}"); - var response = _httpClient.Get(req); - var definition = _deserializer.Deserialize(response.Content); + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + CardigannDefinition definition; + + try + { + var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{id}"); + var response = _httpClient.Get(request); + + definition = _deserializer.Deserialize(response.Content); + } + catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) + { + throw new Exception($"Indexer definition for '{id}' does not exist.", ex); + } + return CleanIndexerDefinition(definition); } @@ -289,7 +311,7 @@ namespace NzbDrone.Core.IndexerVersions EnsureDefinitionsFolder(); var definitionsFolder = Path.Combine(startupFolder, "Definitions"); - var saveFile = Path.Combine(definitionsFolder, $"indexers.zip"); + var saveFile = Path.Combine(definitionsFolder, "indexers.zip"); _httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile); diff --git a/src/NzbDrone.Core/Indexers/Definitions/AlphaRatio.cs b/src/NzbDrone.Core/Indexers/Definitions/AlphaRatio.cs index 03123bb7a..c11d57b57 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/AlphaRatio.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/AlphaRatio.cs @@ -143,7 +143,7 @@ public class AlphaRatioParser : GazelleParser .AddQueryParam("action", "download") .AddQueryParam("id", torrentId); - if (Settings.UseFreeleechToken && canUseToken) + if (Settings.UseFreeleechToken is (int)GazelleFreeleechTokenAction.Preferred or (int)GazelleFreeleechTokenAction.Required && canUseToken) { url = url.AddQueryParam("usetoken", "1"); } @@ -154,9 +154,9 @@ public class AlphaRatioParser : GazelleParser public class AlphaRatioSettings : GazelleSettings { - [FieldDefinition(6, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech torrents only")] + [FieldDefinition(6, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerAlphaRatioSettingsFreeleechOnlyHelpText")] public bool FreeleechOnly { get; set; } - [FieldDefinition(7, Label = "Exclude Scene", Type = FieldType.Checkbox, HelpText = "Exclude Scene torrents from results")] + [FieldDefinition(7, Label = "IndexerAlphaRatioSettingsExcludeScene", Type = FieldType.Checkbox, HelpText = "IndexerAlphaRatioSettingsExcludeSceneHelpText")] public bool ExcludeScene { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs b/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs index 874f87091..fc4add880 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Indexers.Definitions { TvSearchParams = new List { - TvSearchParam.Q + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep }, MusicSearchParams = new List { @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories)); return pageableRequests; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Anidub.cs b/src/NzbDrone.Core/Indexers/Definitions/Anidub.cs index 010b37bc9..26dabda0d 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Anidub.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Anidub.cs @@ -18,6 +18,7 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Definitions { @@ -43,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IParseIndexerResponse GetParser() { - return new AnidubParser(Settings, Capabilities.Categories, RateLimit, _httpClient, _logger); + return new AnidubParser(Definition, Settings, Capabilities.Categories, RateLimit, _httpClient, _logger); } protected override async Task DoLogin() @@ -244,6 +245,7 @@ namespace NzbDrone.Core.Indexers.Definitions public class AnidubParser : IParseIndexerResponse { + private readonly ProviderDefinition _definition; private readonly UserPassTorrentBaseSettings _settings; private readonly IndexerCapabilitiesCategories _categories; private readonly TimeSpan _rateLimit; @@ -270,8 +272,9 @@ namespace NzbDrone.Core.Indexers.Definitions { "/anons_ongoing", "12" } }; - public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger) + public AnidubParser(ProviderDefinition definition, UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger) { + _definition = definition; _settings = settings; _categories = categories; _rateLimit = rateLimit; @@ -479,7 +482,7 @@ namespace NzbDrone.Core.Indexers.Definitions .Build(); var releaseIndexerRequest = new IndexerRequest(releaseRequest); - var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest)); + var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.ExecuteProxied(releaseIndexerRequest.HttpRequest, _definition)); // Throw common http errors here before we try to parse if (releaseResponse.HttpResponse.HasHttpError) diff --git a/src/NzbDrone.Core/Indexers/Definitions/AnimeBytes.cs b/src/NzbDrone.Core/Indexers/Definitions/AnimeBytes.cs index 830b0e6f5..f45ff4173 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/AnimeBytes.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/AnimeBytes.cs @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Indexers.Definitions if (searchCriteria.IsRssSearch) { - cleanReleases = cleanReleases.Where(r => r.PublishDate > DateTime.Now.AddDays(-1)).ToList(); + cleanReleases = cleanReleases.Where((r, index) => r.PublishDate > DateTime.UtcNow.AddDays(-1) || index < 20).ToList(); } return cleanReleases.Select(r => (ReleaseInfo)r.Clone()).ToList(); @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Indexers.Definitions }, MusicSearchParams = new List { - MusicSearchParam.Q + MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year }, BookSearchParams = new List { @@ -209,7 +209,31 @@ namespace NzbDrone.Core.Indexers.Definitions } } - var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories); + if (searchType == "music" && searchCriteria is MusicSearchCriteria musicSearchCriteria) + { + if (musicSearchCriteria.Artist.IsNotNullOrWhiteSpace() && musicSearchCriteria.Artist != "VA") + { + parameters.Set("artistnames", musicSearchCriteria.Artist); + } + + if (musicSearchCriteria.Album.IsNotNullOrWhiteSpace()) + { + parameters.Set("groupname", musicSearchCriteria.Album); + } + + if (musicSearchCriteria.Year is > 0) + { + parameters.Set("year", musicSearchCriteria.Year.ToString()); + } + } + + var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList(); + + if (queryCats.Any() && searchCriteria is TvSearchCriteria { Season: > 0 }) + { + // Avoid searching for specials if it's a non-zero season search + queryCats.RemoveAll(cat => cat is "anime[tv_special]" or "anime[ova]" or "anime[dvd_special]" or "anime[bd_special]"); + } if (queryCats.Any()) { @@ -228,9 +252,7 @@ namespace NzbDrone.Core.Indexers.Definitions searchUrl += "?" + parameters.GetQueryString(); - var request = new IndexerRequest(searchUrl, HttpAccept.Json); - - yield return request; + yield return new IndexerRequest(searchUrl, HttpAccept.Json); } private static string CleanSearchTerm(string term) @@ -279,6 +301,8 @@ namespace NzbDrone.Core.Indexers.Definitions }; private static readonly HashSet ExcludedFileExtensions = new (StringComparer.OrdinalIgnoreCase) { ".mka", ".mds", ".md5", ".nfo", ".sfv", ".ass", ".mks", ".srt", ".ssa", ".sup", ".jpeg", ".jpg", ".png", ".otf", ".ttf" }; + private static readonly string[] PropertiesSeparator = { " | ", " / " }; + private readonly AnimeBytesSettings _settings; public AnimeBytesParser(AnimeBytesSettings settings) @@ -302,6 +326,11 @@ namespace NzbDrone.Core.Indexers.Definitions var response = STJson.Deserialize(indexerResponse.Content); + if (response.Error.IsNotNullOrWhiteSpace()) + { + throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", response.Error); + } + if (response.Matches == 0) { return releaseInfos.ToArray(); @@ -321,7 +350,7 @@ namespace NzbDrone.Core.Indexers.Definitions mainTitle = seriesName; } - var synonyms = new HashSet + var synonyms = new HashSet(StringComparer.OrdinalIgnoreCase) { mainTitle }; @@ -371,38 +400,48 @@ namespace NzbDrone.Core.Indexers.Definitions var minimumSeedTime = 259200 + (int)(size / (int)Math.Pow(1024, 3) * 18000); var propertyList = WebUtility.HtmlDecode(torrent.Property) - .Split(new[] { " | ", " / " }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Split(PropertiesSeparator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .ToList(); propertyList.RemoveAll(p => ExcludedProperties.Any(p.ContainsIgnoreCase)); var properties = propertyList.ToHashSet(); - if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux"))) - { - var resolutionProperty = properties.FirstOrDefault(RemuxResolutions.ContainsIgnoreCase); - - if (resolutionProperty.IsNotNullOrWhiteSpace()) - { - properties.Add($"{resolutionProperty} Remux"); - } - } - - if (properties.Any(p => p.StartsWithIgnoreCase("M2TS"))) + if (properties.Any(p => p.StartsWith("M2TS", StringComparison.Ordinal))) { properties.Add("BR-DISK"); } - if (_settings.ExcludeRaw && - properties.Any(p => p.StartsWithIgnoreCase("RAW") || p.Contains("BR-DISK"))) + var isBluRayDisk = properties.Any(p => p.Equals("RAW", StringComparison.Ordinal) || p.StartsWith("M2TS", StringComparison.Ordinal) || p.StartsWith("ISO", StringComparison.Ordinal)); + + if (_settings.ExcludeRaw && isBluRayDisk) { continue; } + properties = properties + .Select(property => + { + if (isBluRayDisk) + { + property = Regex.Replace(property, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled | RegexOptions.IgnoreCase); + property = Regex.Replace(property, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + + if (torrent.Files.Any(f => f.FileName.ContainsIgnoreCase("Remux")) + && RemuxResolutions.ContainsIgnoreCase(property)) + { + property += " Remux"; + } + + return property; + }) + .ToHashSet(); + int? season = null; int? episode = null; var releaseInfo = _settings.EnableSonarrCompatibility && categoryName == "Anime" ? "S01" : ""; - var editionTitle = torrent.EditionData.EditionTitle; + var editionTitle = torrent.EditionData?.EditionTitle; if (editionTitle.IsNotNullOrWhiteSpace()) { @@ -449,6 +488,11 @@ namespace NzbDrone.Core.Indexers.Definitions // Ignore these categories as they'll cause hell with the matcher // TV Special, DVD Special, BD Special + if (groupName is "TV Special" or "DVD Special" or "BD Special") + { + continue; + } + if (groupName is "TV Series" or "OVA" or "ONA") { categories = new List { NewznabStandardCategory.TVAnime }; @@ -542,7 +586,7 @@ namespace NzbDrone.Core.Indexers.Definitions if (_settings.UseFilenameForSingleEpisodes) { - var files = torrent.Files; + var files = torrent.Files.ToList(); if (files.Count > 1) { @@ -580,11 +624,13 @@ namespace NzbDrone.Core.Indexers.Definitions } } + var useYearInTitle = year is > 0 && torrent.Files.Any(f => f.FileName.Contains(year.Value.ToString())); + foreach (var title in synonyms) { var releaseTitle = groupName is "Movie" or "Live Action Movie" ? $"{releaseGroup}{title} {year} {infoString}" : - $"{releaseGroup}{title} {releaseInfo} {infoString}"; + $"{releaseGroup}{title}{(useYearInTitle ? $" {year}" : string.Empty)} {releaseInfo} {infoString}"; var guid = new Uri(details + "?nh=" + HashUtil.CalculateMd5(title)); @@ -621,16 +667,16 @@ namespace NzbDrone.Core.Indexers.Definitions private static int? ParseSeasonFromTitles(IReadOnlyCollection titles) { - var advancedSeasonRegex = new Regex(@"(\d+)(st|nd|rd|th) Season", RegexOptions.Compiled | RegexOptions.IgnoreCase); + var advancedSeasonRegex = new Regex(@"\b(?:(?\d+)(?:st|nd|rd|th) Season|Season (?\d+))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); var seasonCharactersRegex = new Regex(@"(I{2,})$", RegexOptions.Compiled); - var seasonNumberRegex = new Regex(@"\b(?:S)?([2-9])$", RegexOptions.Compiled); + var seasonNumberRegex = new Regex(@"\b(?[2-9])$", RegexOptions.Compiled); foreach (var title in titles) { var advancedSeasonRegexMatch = advancedSeasonRegex.Match(title); if (advancedSeasonRegexMatch.Success) { - return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups[1].Value); + return ParseUtil.CoerceInt(advancedSeasonRegexMatch.Groups["season"].Value); } var seasonCharactersRegexMatch = seasonCharactersRegex.Match(title); @@ -642,7 +688,7 @@ namespace NzbDrone.Core.Indexers.Definitions var seasonNumberRegexMatch = seasonNumberRegex.Match(title); if (seasonNumberRegexMatch.Success) { - return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups[1].Value); + return ParseUtil.CoerceInt(seasonNumberRegexMatch.Groups["season"].Value); } } @@ -677,7 +723,7 @@ namespace NzbDrone.Core.Indexers.Definitions ExcludeHentai = false; SearchByYear = false; EnableSonarrCompatibility = true; - UseFilenameForSingleEpisodes = false; + UseFilenameForSingleEpisodes = true; AddJapaneseTitle = true; AddRomajiTitle = true; AddAlternativeTitle = true; @@ -728,7 +774,9 @@ namespace NzbDrone.Core.Indexers.Definitions public int Matches { get; set; } [JsonPropertyName("Groups")] - public AnimeBytesGroup[] Groups { get; set; } + public IReadOnlyCollection Groups { get; set; } + + public string Error { get; set; } } public class AnimeBytesGroup @@ -756,16 +804,16 @@ namespace NzbDrone.Core.Indexers.Definitions public string Image { get; set; } [JsonPropertyName("SynonymnsV2")] - public Dictionary Synonymns { get; set; } + public IReadOnlyDictionary Synonymns { get; set; } [JsonPropertyName("Description")] public string Description { get; set; } [JsonPropertyName("Tags")] - public List Tags { get; set; } + public IReadOnlyCollection Tags { get; set; } [JsonPropertyName("Torrents")] - public List Torrents { get; set; } + public IReadOnlyCollection Torrents { get; set; } } public class AnimeBytesTorrent @@ -804,7 +852,7 @@ namespace NzbDrone.Core.Indexers.Definitions public int FileCount { get; set; } [JsonPropertyName("FileList")] - public List Files { get; set; } + public IReadOnlyCollection Files { get; set; } [JsonPropertyName("UploadTime")] public string UploadTime { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs b/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs index 7ba07ab80..14b64979b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; using AngleSharp.Html.Parser; using NLog; using NzbDrone.Common.Extensions; @@ -44,46 +43,19 @@ namespace NzbDrone.Core.Indexers.Definitions return new AnimeTorrentsParser(Settings, Capabilities.Categories); } - protected override async Task DoLogin() - { - UpdateCookies(null, null); - - var loginUrl = Settings.BaseUrl + "login.php"; - - var loginPage = await ExecuteAuth(new HttpRequest(loginUrl)); - - var requestBuilder = new HttpRequestBuilder(loginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true - }; - - var authLoginRequest = requestBuilder - .Post() - .SetCookies(loginPage.GetCookies()) - .AddFormParameter("username", Settings.Username) - .AddFormParameter("password", Settings.Password) - .AddFormParameter("form", "login") - .AddFormParameter("rememberme[]", "1") - .SetHeader("Content-Type", "application/x-www-form-urlencoded") - .SetHeader("Referer", loginUrl) - .Build(); - - var response = await ExecuteAuth(authLoginRequest); - - if (response.Content == null || !response.Content.Contains("logout.php")) - { - throw new IndexerAuthException("AnimeTorrents authentication failed"); - } - - UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30)); - - _logger.Debug("AnimeTorrents authentication succeeded"); - } - protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) { - return httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php"); + if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php")) + { + throw new IndexerAuthException("AnimeTorrents authentication with cookies failed."); + } + + return false; + } + + protected override IDictionary GetCookies() + { + return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); } private IndexerCapabilities SetCapabilities() @@ -119,6 +91,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.BooksComics, "Doujinshi"); caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.BooksComics, "Doujinshi 18+"); caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Audio, "OST"); + caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.AudioAudiobook, "Audiobooks"); return caps; } @@ -291,7 +264,7 @@ namespace NzbDrone.Core.Indexers.Definitions var qTitleLink = row.QuerySelector("td:nth-of-type(2) a:nth-of-type(1)"); var title = qTitleLink?.TextContent.Trim(); - // If we search an get no results, we still get a table just with no info. + // If we search and get no results, we still get a table just with no info. if (title.IsNullOrWhiteSpace()) { break; @@ -306,6 +279,8 @@ namespace NzbDrone.Core.Indexers.Definitions var connections = row.QuerySelector("td:nth-of-type(8)").TextContent.Trim().Split('/', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var seeders = ParseUtil.CoerceInt(connections[0]); + var leechers = ParseUtil.CoerceInt(connections[1]); + var grabs = ParseUtil.CoerceInt(connections[2]); var categoryLink = row.QuerySelector("td:nth-of-type(1) a")?.GetAttribute("href") ?? string.Empty; var categoryId = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat"); @@ -327,17 +302,17 @@ namespace NzbDrone.Core.Indexers.Definitions PublishDate = publishedDate, Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(6)").TextContent.Trim()), Seeders = seeders, - Peers = ParseUtil.CoerceInt(connections[1]) + seeders, - Grabs = ParseUtil.CoerceInt(connections[2]), + Peers = leechers + seeders, + Grabs = grabs, DownloadVolumeFactor = downloadVolumeFactor, UploadVolumeFactor = 1, Genres = row.QuerySelectorAll("td:nth-of-type(2) a.tortags").Select(t => t.TextContent.Trim()).ToList() }; - var uLFactorImg = row.QuerySelector("img[alt*=\"x Multiplier Torrent\"]"); - if (uLFactorImg != null) + var uploadFactor = row.QuerySelector("img[alt*=\"x Multiplier Torrent\"]")?.GetAttribute("alt"); + if (uploadFactor != null) { - release.UploadVolumeFactor = ParseUtil.CoerceDouble(uLFactorImg.GetAttribute("alt").Split('x')[0]); + release.UploadVolumeFactor = ParseUtil.CoerceDouble(uploadFactor.Split('x')[0]); } releaseInfos.Add(release); @@ -349,7 +324,7 @@ namespace NzbDrone.Core.Indexers.Definitions public Action, DateTime?> CookiesUpdater { get; set; } } - public class AnimeTorrentsSettings : UserPassTorrentBaseSettings + public class AnimeTorrentsSettings : CookieTorrentBaseSettings { public AnimeTorrentsSettings() { @@ -360,7 +335,7 @@ namespace NzbDrone.Core.Indexers.Definitions [FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech torrents only")] public bool FreeleechOnly { get; set; } - [FieldDefinition(5, Label = "Downloadable Only", Type = FieldType.Checkbox, HelpText = "Search downloadable torrents only (enable this only if your account class is Newbie)")] + [FieldDefinition(5, Label = "Downloadable Only", Type = FieldType.Checkbox, HelpText = "Search downloadable torrents only (enable this only if your account class is Newbie)", Advanced = true)] public bool DownloadableOnly { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Animedia.cs b/src/NzbDrone.Core/Indexers/Definitions/Animedia.cs index 8d08c03b3..9f898b1f9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Animedia.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Animedia.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Definitions { @@ -40,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IParseIndexerResponse GetParser() { - return new AnimediaParser(Settings, Capabilities.Categories, RateLimit, _httpClient); + return new AnimediaParser(Definition, Settings, Capabilities.Categories, RateLimit, _httpClient); } private IndexerCapabilities SetCapabilities() @@ -144,6 +145,7 @@ namespace NzbDrone.Core.Indexers.Definitions public class AnimediaParser : IParseIndexerResponse { + private readonly ProviderDefinition _definition; private readonly NoAuthTorrentBaseSettings _settings; private readonly IndexerCapabilitiesCategories _categories; private readonly TimeSpan _rateLimit; @@ -157,8 +159,9 @@ namespace NzbDrone.Core.Indexers.Definitions private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient) + public AnimediaParser(ProviderDefinition definition, NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient) { + _definition = definition; _settings = settings; _categories = categories; _rateLimit = rateLimit; @@ -311,7 +314,7 @@ namespace NzbDrone.Core.Indexers.Definitions .Build(); var releaseIndexerRequest = new IndexerRequest(releaseRequest); - var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest)); + var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.ExecuteProxied(releaseIndexerRequest.HttpRequest, _definition)); // Throw common http errors here before we try to parse if (releaseResponse.HttpResponse.HasHttpError) diff --git a/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs b/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs deleted file mode 100644 index 78465e54b..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions -{ - [Obsolete("Moved to YML for Cardigann")] - public class Anthelion : TorrentIndexerBase - { - public override string Name => "Anthelion"; - public override string[] IndexerUrls => new string[] { "https://anthelion.me/" }; - private string LoginUrl => Settings.BaseUrl + "login.php"; - public override string Description => "A movies tracker"; - public override string Language => "en-US"; - public override Encoding Encoding => Encoding.UTF8; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override IndexerCapabilities Capabilities => SetCapabilities(); - - public Anthelion(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new AnthelionRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; - } - - public override IParseIndexerResponse GetParser() - { - return new AnthelionParser(Settings, Capabilities.Categories); - } - - protected override async Task DoLogin() - { - var requestBuilder = new HttpRequestBuilder(LoginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true, - Method = HttpMethod.Post - }; - - var cookies = Cookies; - Cookies = null; - - var authLoginRequest = requestBuilder - .AddFormParameter("username", Settings.Username) - .AddFormParameter("password", Settings.Password) - .AddFormParameter("keeplogged", "1") - .AddFormParameter("login", "Log+In!") - .SetHeader("Content-Type", "application/x-www-form-urlencoded") - .SetHeader("Referer", LoginUrl) - .Build(); - - var response = await ExecuteAuth(authLoginRequest); - - if (CheckIfLoginNeeded(response)) - { - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(response.Content); - var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim(); - - throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report."); - } - - cookies = response.GetCookies(); - UpdateCookies(cookies, DateTime.Now.AddDays(30)); - - _logger.Debug("Anthelion authentication succeeded."); - } - - protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) - { - return !httpResponse.Content.Contains("logout.php"); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep - }, - MovieSearchParams = new List - { - MovieSearchParam.Q - } - }; - - caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature"); - caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.Movies, "Film/Short"); - caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TV, "TV/Miniseries"); - caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Other, "Other"); - - return caps; - } - } - - public class AnthelionRequestGenerator : IIndexerRequestGenerator - { - public UserPassTorrentBaseSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - - private IEnumerable GetPagedRequests(string term, int[] categories, string imdbId = null) - { - var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/')); - - // TODO: IMDB search is available but it requires to parse the details page - var qc = new NameValueCollection - { - { "order_by", "time" }, - { "order_way", "desc" }, - { "action", "basic" }, - { "searchsubmit", "1" }, - { "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term.Replace(".", " ") } - }; - - var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories); - - foreach (var cat in catList) - { - qc.Add($"filter_cat[{cat}]", "1"); - } - - searchUrl = searchUrl + "?" + qc.GetQueryString(); - - var request = new IndexerRequest(searchUrl, HttpAccept.Html); - - yield return request; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - public Func> GetCookies { get; set; } - public Action, DateTime?> CookiesUpdater { get; set; } - } - - public class AnthelionParser : IParseIndexerResponse - { - private readonly UserPassTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; - - public AnthelionParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) - { - _settings = settings; - _categories = categories; - } - - public IList ParseResponse(IndexerResponse indexerResponse) - { - var torrentInfos = new List(); - - var parser = new HtmlParser(); - using var doc = parser.ParseDocument(indexerResponse.Content); - var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent"); - foreach (var row in rows) - { - var qDetailsLink = row.QuerySelector("a.torrent_name"); - var year = qDetailsLink.NextSibling.TextContent.Replace("[", "").Replace("]", "").Trim(); - var tags = row.QuerySelector("div.torrent_info").FirstChild.TextContent.Replace(" / ", " ").Trim(); - var title = $"{qDetailsLink.TextContent} {year} {tags}"; - var description = row.QuerySelector("div.tags").TextContent.Trim(); - var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href"); - var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last(); - var link = _settings.BaseUrl + "torrents.php?action=download&id=" + torrentId; - var posterStr = qDetailsLink.GetAttribute("data-cover"); - var poster = !string.IsNullOrWhiteSpace(posterStr) ? posterStr : null; - - var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent); - var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent); - var size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent); - var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent); - var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent); - var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent); - - var dlVolumeFactor = row.QuerySelector("strong.tl_free") != null ? 0 : 1; - - var cat = row.QuerySelector("td.cats_col > div").GetAttribute("class").Replace("tooltip cats_", ""); - var category = new List - { - cat switch - { - "featurefilm" => NewznabStandardCategory.Movies, - "shortfilm" => NewznabStandardCategory.Movies, - "miniseries" => NewznabStandardCategory.TV, - "other" => NewznabStandardCategory.Other, - _ => throw new Exception($"Unknown category: {cat}") - } - }; - - // TODO: TMDb is also available - var qImdb = row.QuerySelector("a[href^=\"https://www.imdb.com\"]"); - var imdb = qImdb != null ? ParseUtil.GetImdbId(qImdb.GetAttribute("href").Split('/').Last()) : null; - - var release = new TorrentInfo - { - MinimumRatio = 1, - MinimumSeedTime = 259200, - Description = description, - Title = title, - PublishDate = publishDate, - Categories = category, - DownloadUrl = link, - InfoUrl = details, - PosterUrl = poster, - Guid = link, - ImdbId = imdb.GetValueOrDefault(), - Seeders = seeders, - Peers = leechers + seeders, - Size = size, - Grabs = grabs, - Files = files, - DownloadVolumeFactor = dlVolumeFactor, - UploadVolumeFactor = 1 - }; - - torrentInfos.Add(release); - } - - return torrentInfos.ToArray(); - } - - public Action, DateTime?> CookiesUpdater { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/AroLol.cs b/src/NzbDrone.Core/Indexers/Definitions/AroLol.cs deleted file mode 100644 index 5c12cdfc6..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/AroLol.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Definitions.Gazelle; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Indexers.Definitions; - -public class AroLol : GazelleBase -{ - public override string Name => "aro.lol"; - public override string[] IndexerUrls => new[] { "https://aro.lol/" }; - public override string Description => "aro.lol is a SERBIAN/ENGLISH Private Torrent Tracker for ANIME"; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - - public AroLol(IIndexerHttpClient httpClient, - IEventAggregator eventAggregator, - IIndexerStatusService indexerStatusService, - IConfigService configService, - Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - protected override HttpRequestBuilder AuthLoginRequestBuilder() - { - return base.AuthLoginRequestBuilder() - .AddFormParameter("twofa", Settings.TwoFactorAuthCode?.Trim() ?? ""); - } - - protected override bool CheckForLoginError(HttpResponse response) - { - if (response.Content.Contains("loginform")) - { - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(response.Content); - var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim(); - - throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report."); - } - - return true; - } - - protected override IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List - { - TvSearchParam.Q - }, - MovieSearchParams = new List - { - MovieSearchParam.Q - }, - BookSearchParams = new List - { - BookSearchParam.Q - } - }; - - caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Movies"); - caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVAnime, "Anime"); - caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.Books, "Manga"); - caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Console, "Games"); - caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.Other, "Other"); - - return caps; - } -} - -public class AroLolSettings : GazelleSettings -{ - [FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the 2FA code box if you have enabled 2FA on the aro.lol Web Site. Otherwise just leave it empty.")] - public string TwoFactorAuthCode { get; set; } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs b/src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs deleted file mode 100644 index a416510bc..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/AudioBookBay.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using AngleSharp.Html.Dom; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions; - -[Obsolete("User Agent blocked")] -public class AudioBookBay : TorrentIndexerBase -{ - public override string Name => "AudioBook Bay"; - public override string[] IndexerUrls => new[] - { - "https://audiobookbay.is/", - "https://audiobookbay.se/" - }; - public override string[] LegacyUrls => new[] - { - "https://audiobookbay.la/", - "http://audiobookbay.net/", - "https://audiobookbay.unblockit.tv/", - "http://audiobookbay.nl/", - "http://audiobookbay.ws/", - "https://audiobookbay.unblockit.how/", - "https://audiobookbay.unblockit.cam/", - "https://audiobookbay.unblockit.biz/", - "https://audiobookbay.unblockit.day/", - "https://audiobookbay.unblockit.llc/", - "https://audiobookbay.unblockit.blue/", - "https://audiobookbay.unblockit.name/", - "http://audiobookbay.fi/", - "http://audiobookbay.se/", - "http://audiobookbayabb.com/", - "https://audiobookbay.unblockit.ist/", - "https://audiobookbay.unblockit.bet/", - "https://audiobookbay.unblockit.cat/", - "https://audiobookbay.unblockit.nz/", - "https://audiobookbay.fi/", - "https://audiobookbay.unblockit.page/", - "https://audiobookbay.unblockit.pet/", - "https://audiobookbay.unblockit.ink/", - "https://audiobookbay.unblockit.bio/", // error 502 - "https://audiobookbay.li/" - }; - public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS"; - public override string Language => "en-US"; - public override IndexerPrivacy Privacy => IndexerPrivacy.Public; - public override int PageSize => 15; - public override IndexerCapabilities Capabilities => SetCapabilities(); - - public AudioBookBay(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new AudioBookBayRequestGenerator(Settings, Capabilities); - } - - public override IParseIndexerResponse GetParser() - { - return new AudioBookBayParser(Settings, Capabilities.Categories); - } - - public override async Task Download(Uri link) - { - var request = new HttpRequestBuilder(link.ToString()) - .SetCookies(GetCookies() ?? new Dictionary()) - .Accept(HttpAccept.Html) - .Build(); - - var response = await _httpClient.ExecuteProxiedAsync(request, Definition); - - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(response.Content); - - var hash = dom.QuerySelector("td:contains(\"Info Hash:\") ~ td")?.TextContent.Trim(); - if (hash == null) - { - throw new Exception($"Failed to fetch hash from {link}"); - } - - var title = dom.QuerySelector("div.postTitle h1")?.TextContent.Trim(); - if (title == null) - { - throw new Exception($"Failed to fetch title from {link}"); - } - - title = StringUtil.MakeValidFileName(title, '_', false); - - var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title); - - return await base.Download(new Uri(magnet)); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - BookSearchParams = new List - { - BookSearchParam.Q - } - }; - - // Age - caps.Categories.AddCategoryMapping("children", NewznabStandardCategory.AudioAudiobook, "Children"); - caps.Categories.AddCategoryMapping("teen-young-adult", NewznabStandardCategory.AudioAudiobook, "Teen & Young Adult"); - caps.Categories.AddCategoryMapping("adults", NewznabStandardCategory.AudioAudiobook, "Adults"); - - // Category - caps.Categories.AddCategoryMapping("postapocalyptic", NewznabStandardCategory.AudioAudiobook, "(Post)apocalyptic"); - caps.Categories.AddCategoryMapping("action", NewznabStandardCategory.AudioAudiobook, "Action"); - caps.Categories.AddCategoryMapping("adventure", NewznabStandardCategory.AudioAudiobook, "Adventure"); - caps.Categories.AddCategoryMapping("art", NewznabStandardCategory.AudioAudiobook, "Art"); - caps.Categories.AddCategoryMapping("autobiography-biographies", NewznabStandardCategory.AudioAudiobook, "Autobiography & Biographies"); - caps.Categories.AddCategoryMapping("business", NewznabStandardCategory.AudioAudiobook, "Business"); - caps.Categories.AddCategoryMapping("computer", NewznabStandardCategory.AudioAudiobook, "Computer"); - caps.Categories.AddCategoryMapping("contemporary", NewznabStandardCategory.AudioAudiobook, "Contemporary"); - caps.Categories.AddCategoryMapping("crime", NewznabStandardCategory.AudioAudiobook, "Crime"); - caps.Categories.AddCategoryMapping("detective", NewznabStandardCategory.AudioAudiobook, "Detective"); - caps.Categories.AddCategoryMapping("doctor-who-sci-fi", NewznabStandardCategory.AudioAudiobook, "Doctor Who"); - caps.Categories.AddCategoryMapping("education", NewznabStandardCategory.AudioAudiobook, "Education"); - caps.Categories.AddCategoryMapping("fantasy", NewznabStandardCategory.AudioAudiobook, "Fantasy"); - caps.Categories.AddCategoryMapping("general-fiction", NewznabStandardCategory.AudioAudiobook, "General Fiction"); - caps.Categories.AddCategoryMapping("historical-fiction", NewznabStandardCategory.AudioAudiobook, "Historical Fiction"); - caps.Categories.AddCategoryMapping("history", NewznabStandardCategory.AudioAudiobook, "History"); - caps.Categories.AddCategoryMapping("horror", NewznabStandardCategory.AudioAudiobook, "Horror"); - caps.Categories.AddCategoryMapping("humor", NewznabStandardCategory.AudioAudiobook, "Humor"); - caps.Categories.AddCategoryMapping("lecture", NewznabStandardCategory.AudioAudiobook, "Lecture"); - caps.Categories.AddCategoryMapping("lgbt", NewznabStandardCategory.AudioAudiobook, "LGBT"); - caps.Categories.AddCategoryMapping("literature", NewznabStandardCategory.AudioAudiobook, "Literature"); - caps.Categories.AddCategoryMapping("litrpg", NewznabStandardCategory.AudioAudiobook, "LitRPG"); - caps.Categories.AddCategoryMapping("general-non-fiction", NewznabStandardCategory.AudioAudiobook, "Misc. Non-fiction"); - caps.Categories.AddCategoryMapping("mystery", NewznabStandardCategory.AudioAudiobook, "Mystery"); - caps.Categories.AddCategoryMapping("paranormal", NewznabStandardCategory.AudioAudiobook, "Paranormal"); - caps.Categories.AddCategoryMapping("plays-theater", NewznabStandardCategory.AudioAudiobook, "Plays & Theater"); - caps.Categories.AddCategoryMapping("poetry", NewznabStandardCategory.AudioAudiobook, "Poetry"); - caps.Categories.AddCategoryMapping("political", NewznabStandardCategory.AudioAudiobook, "Political"); - caps.Categories.AddCategoryMapping("radio-productions", NewznabStandardCategory.AudioAudiobook, "Radio Productions"); - caps.Categories.AddCategoryMapping("romance", NewznabStandardCategory.AudioAudiobook, "Romance"); - caps.Categories.AddCategoryMapping("sci-fi", NewznabStandardCategory.AudioAudiobook, "Sci-Fi"); - caps.Categories.AddCategoryMapping("science", NewznabStandardCategory.AudioAudiobook, "Science"); - caps.Categories.AddCategoryMapping("self-help", NewznabStandardCategory.AudioAudiobook, "Self-help"); - caps.Categories.AddCategoryMapping("spiritual", NewznabStandardCategory.AudioAudiobook, "Spiritual & Religious"); - caps.Categories.AddCategoryMapping("sports", NewznabStandardCategory.AudioAudiobook, "Sport & Recreation"); - caps.Categories.AddCategoryMapping("suspense", NewznabStandardCategory.AudioAudiobook, "Suspense"); - caps.Categories.AddCategoryMapping("thriller", NewznabStandardCategory.AudioAudiobook, "Thriller"); - caps.Categories.AddCategoryMapping("true-crime", NewznabStandardCategory.AudioAudiobook, "True Crime"); - caps.Categories.AddCategoryMapping("tutorial", NewznabStandardCategory.AudioAudiobook, "Tutorial"); - caps.Categories.AddCategoryMapping("westerns", NewznabStandardCategory.AudioAudiobook, "Westerns"); - caps.Categories.AddCategoryMapping("zombies", NewznabStandardCategory.AudioAudiobook, "Zombies"); - - // Category Modifiers - caps.Categories.AddCategoryMapping("anthology", NewznabStandardCategory.AudioAudiobook, "Anthology"); - caps.Categories.AddCategoryMapping("bestsellers", NewznabStandardCategory.AudioAudiobook, "Bestsellers"); - caps.Categories.AddCategoryMapping("classic", NewznabStandardCategory.AudioAudiobook, "Classic"); - caps.Categories.AddCategoryMapping("documentary", NewznabStandardCategory.AudioAudiobook, "Documentary"); - caps.Categories.AddCategoryMapping("full-cast", NewznabStandardCategory.AudioAudiobook, "Full Cast"); - caps.Categories.AddCategoryMapping("libertarian", NewznabStandardCategory.AudioAudiobook, "Libertarian"); - caps.Categories.AddCategoryMapping("military", NewznabStandardCategory.AudioAudiobook, "Military"); - caps.Categories.AddCategoryMapping("novel", NewznabStandardCategory.AudioAudiobook, "Novel"); - caps.Categories.AddCategoryMapping("short-story", NewznabStandardCategory.AudioAudiobook, "Short Story"); - - return caps; - } -} - -public class AudioBookBayRequestGenerator : IIndexerRequestGenerator -{ - private readonly NoAuthTorrentBaseSettings _settings; - private readonly IndexerCapabilities _capabilities; - - public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings, IndexerCapabilities capabilities) - { - _settings = settings; - _capabilities = capabilities; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}")); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}")); - - return pageableRequests; - } - - private IEnumerable GetPagedRequests(string term) - { - var searchUrl = _settings.BaseUrl; - - var parameters = new NameValueCollection(); - - term = Regex.Replace(term, @"[\W]+", " ").Trim(); - - if (term.IsNotNullOrWhiteSpace()) - { - parameters.Set("s", term); - parameters.Set("tt", "1"); - } - - if (parameters.Count > 0) - { - searchUrl += $"?{parameters.GetQueryString()}"; - } - - yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/" }.Uri.AbsoluteUri, HttpAccept.Html); - yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/2/" }.Uri.AbsoluteUri, HttpAccept.Html); - yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/3/" }.Uri.AbsoluteUri, HttpAccept.Html); - } - - public Func> GetCookies { get; set; } - public Action, DateTime?> CookiesUpdater { get; set; } -} - -public class AudioBookBayParser : IParseIndexerResponse -{ - private readonly NoAuthTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; - - public AudioBookBayParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) - { - _settings = settings; - _categories = categories; - } - - public IList ParseResponse(IndexerResponse indexerResponse) - { - var releaseInfos = new List(); - - using var doc = ParseHtmlDocument(indexerResponse.Content); - - var rows = doc.QuerySelectorAll("div.post:has(div[class=\"postTitle\"])"); - foreach (var row in rows) - { - var infoUrl = _settings.BaseUrl + row.QuerySelector("div.postTitle h2 a")?.GetAttribute("href")?.Trim().TrimStart('/'); - - var title = row.QuerySelector("div.postTitle")?.TextContent.Trim(); - - var infoString = row.QuerySelector("div.postContent")?.TextContent.Trim() ?? string.Empty; - - var matchFormat = Regex.Match(infoString, @"Format: (.+) \/", RegexOptions.IgnoreCase); - if (matchFormat.Groups[1].Success && matchFormat.Groups[1].Value.Length > 0 && matchFormat.Groups[1].Value != "?") - { - title += $" [{matchFormat.Groups[1].Value.Trim()}]"; - } - - var matchBitrate = Regex.Match(infoString, @"Bitrate: (.+)File", RegexOptions.IgnoreCase); - if (matchBitrate.Groups[1].Success && matchBitrate.Groups[1].Value.Length > 0 && matchBitrate.Groups[1].Value != "?") - { - title += $" [{matchBitrate.Groups[1].Value.Trim()}]"; - } - - var matchSize = Regex.Match(infoString, @"File Size: (.+?)s?$", RegexOptions.IgnoreCase); - var size = matchSize.Groups[1].Success ? ParseUtil.GetBytes(matchSize.Groups[1].Value) : 0; - - var matchDateAdded = Regex.Match(infoString, @"Posted: (\d{1,2} \D{3} \d{4})", RegexOptions.IgnoreCase); - var publishDate = matchDateAdded.Groups[1].Success && DateTime.TryParseExact(matchDateAdded.Groups[1].Value, "d MMM yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDate) ? parsedDate : DateTime.Now; - - var postInfo = row.QuerySelector("div.postInfo")?.FirstChild?.TextContent.Trim().Replace("\xA0", ";") ?? string.Empty; - var matchCategory = Regex.Match(postInfo, @"Category: (.+)$", RegexOptions.IgnoreCase); - var category = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List(); - var categories = category.SelectMany(_categories.MapTrackerCatDescToNewznab).Distinct().ToList(); - - var release = new TorrentInfo - { - Guid = infoUrl, - InfoUrl = infoUrl, - DownloadUrl = infoUrl, - Title = CleanTitle(title), - Categories = categories, - Size = size, - Seeders = 1, - Peers = 1, - PublishDate = publishDate, - DownloadVolumeFactor = 0, - UploadVolumeFactor = 1 - }; - - var cover = row.QuerySelector("img[src]")?.GetAttribute("src")?.Trim(); - if (!string.IsNullOrEmpty(cover)) - { - release.PosterUrl = cover.StartsWith("http") ? cover : _settings.BaseUrl + cover; - } - - releaseInfos.Add(release); - } - - return releaseInfos; - } - - private static IHtmlDocument ParseHtmlDocument(string response) - { - var parser = new HtmlParser(); - var doc = parser.ParseDocument(response); - - var hidden = doc.QuerySelectorAll("div.post.re-ab"); - foreach (var element in hidden) - { - var body = doc.CreateElement("div"); - body.ClassList.Add("post"); - body.InnerHtml = Encoding.UTF8.GetString(Convert.FromBase64String(element.TextContent)); - element.Parent.ReplaceChild(body, element); - } - - return doc; - } - - private static string CleanTitle(string title) - { - title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled); - title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - return title.Trim(); - } - - public Action, DateTime?> CookiesUpdater { get; set; } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/AvistaZ.cs b/src/NzbDrone.Core/Indexers/Definitions/AvistaZ.cs index 4e1899651..7d9ba3b51 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/AvistaZ.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/AvistaZ.cs @@ -84,6 +84,6 @@ namespace NzbDrone.Core.Indexers.Definitions public class AvistaZParser : AvistazParserBase { - protected override string TimezoneOffset => "+01:00"; + protected override string TimezoneOffset => "+02:00"; } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs index 4d9c7e5fc..b14bac702 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Indexers.Definitions.Avistaz @@ -15,9 +16,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz { public override bool SupportsRss => true; public override bool SupportsSearch => true; - public override bool SupportsPagination => true; + public override bool SupportsPagination => false; public override int PageSize => 50; - public override TimeSpan RateLimit => TimeSpan.FromSeconds(5); + public override TimeSpan RateLimit => TimeSpan.FromSeconds(6); public override IndexerCapabilities Capabilities => SetCapabilities(); protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth"; private IIndexerRepository _indexerRepository; @@ -50,21 +51,28 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz return new AvistazParserBase(); } - protected virtual IndexerCapabilities SetCapabilities() - { - return new IndexerCapabilities(); - } + protected abstract IndexerCapabilities SetCapabilities(); protected override async Task DoLogin() { - Settings.Token = await GetToken(); - - if (Definition.Id > 0) + try { - _indexerRepository.UpdateSettings((IndexerDefinition)Definition); - } + Settings.Token = await GetToken(); - _logger.Debug("Avistaz authentication succeeded."); + if (Definition.Id > 0) + { + _indexerRepository.UpdateSettings((IndexerDefinition)Definition); + } + + _logger.Debug("Avistaz authentication succeeded."); + } + catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Warn(ex, "Failed to authenticate with Avistaz"); + + STJson.TryDeserialize(ex.Response.Content, out var jsonResponse); + throw new IndexerAuthException(jsonResponse?.Message ?? "Unauthorized request to indexer"); + } } protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) @@ -90,8 +98,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz { _logger.Warn(ex, "Unauthorized request to indexer"); - var jsonResponse = new HttpResponse(ex.Response); - return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer"); + STJson.TryDeserialize(ex.Response.Content, out var jsonResponse); + return new ValidationFailure(string.Empty, jsonResponse?.Message ?? "Unauthorized request to indexer"); } _logger.Warn(ex, "Unable to connect to indexer"); @@ -116,16 +124,20 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz Method = HttpMethod.Post }; + // TODO: Change to HttpAccept.Json after they fix the issue with missing headers var authLoginRequest = requestBuilder .AddFormParameter("username", Settings.Username) .AddFormParameter("password", Settings.Password) .AddFormParameter("pid", Settings.Pid.Trim()) - .Accept(HttpAccept.Json) + .Accept(HttpAccept.Html) .Build(); var response = await ExecuteAuth(authLoginRequest); - var authResponse = STJson.Deserialize(response.Content); + if (!STJson.TryDeserialize(response.Content, out var authResponse)) + { + throw new Exception("Invalid response from AvistaZ, the response is not valid JSON"); + } return authResponse.Token; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs index 993efb931..56cc7a3fe 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz { public class AvistazParserBase : IParseIndexerResponse { - protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time + protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time private readonly HashSet _hdResolutions = new () { "1080p", "1080i", "720p" }; public Action, DateTime?> CookiesUpdater { get; set; } @@ -33,6 +33,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached"); } + if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Unauthorized) + { + STJson.TryDeserialize(indexerResponse.HttpResponse.Content, out var errorResponse); + throw new IndexerAuthException(errorResponse?.Message ?? "Unauthorized request to indexer"); + } + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); @@ -69,11 +75,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz DownloadVolumeFactor = row.DownloadMultiply, UploadVolumeFactor = row.UploadMultiply, MinimumRatio = 1, - MinimumSeedTime = 172800, // 48 hours + MinimumSeedTime = 259200, // 72 hours Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List(), Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List() }; + if (row.FileSize is > 0) + { + var sizeGigabytes = row.FileSize.Value / Math.Pow(1024, 3); + + release.MinimumSeedTime = sizeGigabytes switch + { + > 50.0 => (long)((100 * Math.Log(sizeGigabytes)) - 219.2023) * 3600, + _ => 259200 + (long)(sizeGigabytes * 7200) + }; + } + if (row.MovieTvinfo != null) { release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault(); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs index b2740c64a..d2dbc52d1 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs @@ -27,16 +27,16 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz public string Token { get; set; } - [FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)] + [FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] + [FieldDefinition(3, Label = "Password", HelpText = "IndexerAvistazSettingsPasswordHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] + [FieldDefinition(4, Label = "PID", HelpText = "IndexerAvistazSettingsPidHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] public string Pid { get; set; } - [FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")] + [FieldDefinition(5, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerAvistazSettingsFreeleechOnlyHelpText")] public bool FreeleechOnly { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/BB.cs b/src/NzbDrone.Core/Indexers/Definitions/BB.cs deleted file mode 100644 index a2439873f..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/BB.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using AngleSharp.Dom; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions -{ - [Obsolete("Site has shutdown")] - public class BB : TorrentIndexerBase - { - public override string Name => "BB"; - public override string[] IndexerUrls => new[] { Base64Extensions.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") }; - private string LoginUrl => Settings.BaseUrl + "login.php"; - public override string Description => "BB is a Private Torrent Tracker for 0DAY / GENERAL"; - public override string Language => "en-US"; - public override Encoding Encoding => Encoding.UTF8; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override IndexerCapabilities Capabilities => SetCapabilities(); - - public BB(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new BBRequestGenerator { Settings = Settings, Capabilities = Capabilities }; - } - - public override IParseIndexerResponse GetParser() - { - return new BBParser(Settings, Capabilities.Categories); - } - - protected override async Task DoLogin() - { - var requestBuilder = new HttpRequestBuilder(LoginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true, - Method = HttpMethod.Post - }; - - var cookies = Cookies; - Cookies = null; - - var authLoginRequest = requestBuilder - .AddFormParameter("username", Settings.Username) - .AddFormParameter("password", Settings.Password) - .AddFormParameter("keeplogged", "1") - .AddFormParameter("login", "Log+In!") - .SetHeader("Content-Type", "application/x-www-form-urlencoded") - .SetHeader("Referer", LoginUrl) - .Build(); - - var response = await ExecuteAuth(authLoginRequest); - - if (CheckIfLoginNeeded(response)) - { - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(response.Content); - var messageEl = dom.QuerySelectorAll("#loginform"); - var messages = new List(); - for (var i = 0; i < 13; i++) - { - var child = messageEl[0].ChildNodes[i]; - messages.Add(child.Text().Trim()); - } - - var message = string.Join(" ", messages); - - throw new IndexerAuthException(message); - } - - cookies = response.GetCookies(); - UpdateCookies(cookies, DateTime.Now.AddDays(30)); - - _logger.Debug("BB authentication succeeded."); - } - - protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) - { - return !httpResponse.Content.Contains("logout.php"); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep - }, - MovieSearchParams = new List - { - MovieSearchParam.Q - }, - MusicSearchParams = new List - { - MusicSearchParam.Q - }, - BookSearchParams = new List - { - BookSearchParam.Q - } - }; - - caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio); - caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioMP3); - caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioLossless); - caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC); - caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook); - caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioAudiobook); - caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Other); - caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.BooksMags); - caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksComics); - caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TVAnime); - caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.Movies); - caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVHD); - caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVSD); - caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TV); - caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.PCGames); - caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.Console); - caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Other); - caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.Other); - - return caps; - } - } - - public class BBRequestGenerator : IIndexerRequestGenerator - { - public UserPassTorrentBaseSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - - private IEnumerable GetPagedRequests(string term, int[] categories) - { - var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/')); - - // TODO: IMDB search is available but it requires to parse the details page - var qc = new NameValueCollection - { - { "order_by", "s3" }, - { "order_way", "desc" }, - { "disablegrouping", "1" }, - { "searchtags", "" }, - { "tags_type", "0" }, - { "action", "basic" }, - { "searchstr", term.Replace(".", " ") } - }; - - var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories); - - foreach (var cat in catList) - { - qc.Add($"filter_cat[{cat}]", "1"); - } - - searchUrl = searchUrl + "?" + qc.GetQueryString(); - - var request = new IndexerRequest(searchUrl, HttpAccept.Html); - - yield return request; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - public Func> GetCookies { get; set; } - public Action, DateTime?> CookiesUpdater { get; set; } - } - - public class BBParser : IParseIndexerResponse - { - private readonly UserPassTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; - - public BBParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) - { - _settings = settings; - _categories = categories; - } - - public IList ParseResponse(IndexerResponse indexerResponse) - { - var torrentInfos = new List(); - - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(indexerResponse.Content); - var rows = dom.QuerySelectorAll("#torrent_table > tbody > tr.torrent"); - - foreach (var row in rows) - { - var release = new TorrentInfo(); - - release.MinimumRatio = 1; - release.MinimumSeedTime = 172800; // 48 hours - - var catStr = row.Children[0].FirstElementChild.GetAttribute("href").Split(new[] { '[', ']' })[1]; - release.Categories = _categories.MapTrackerCatToNewznab(catStr); - - var qDetails = row.Children[1].QuerySelector("a[title='View Torrent']"); - release.InfoUrl = _settings.BaseUrl + qDetails.GetAttribute("href"); - release.Guid = release.InfoUrl; - - var qDownload = row.Children[1].QuerySelector("a[title='Download']"); - release.DownloadUrl = _settings.BaseUrl + qDownload.GetAttribute("href"); - - var dateStr = row.Children[3].TextContent.Trim().Replace(" and", ""); - release.PublishDate = DateTimeUtil.FromTimeAgo(dateStr); - - var sizeStr = row.Children[4].TextContent; - release.Size = ParseUtil.GetBytes(sizeStr); - - release.Files = ParseUtil.CoerceInt(row.Children[2].TextContent.Trim()); - release.Seeders = ParseUtil.CoerceInt(row.Children[7].TextContent.Trim()); - release.Peers = ParseUtil.CoerceInt(row.Children[8].TextContent.Trim()) + release.Seeders; - - var grabs = row.QuerySelector("td:nth-child(6)").TextContent; - release.Grabs = ParseUtil.CoerceInt(grabs); - - if (row.QuerySelector("strong:contains(\"Freeleech!\")") != null) - { - release.DownloadVolumeFactor = 0; - } - else - { - release.DownloadVolumeFactor = 1; - } - - release.UploadVolumeFactor = 1; - - var title = row.QuerySelector("td:nth-child(2)"); - foreach (var element in title.QuerySelectorAll("span, strong, div, br")) - { - element.Remove(); - } - - release.Title = ParseUtil.NormalizeMultiSpaces(title.TextContent.Replace(" - ]", "]")); - - //change "Season #" to "S##" for TV shows - if (catStr == "10") - { - release.Title = Regex.Replace(release.Title, - @"Season (\d+)", - m => string.Format("S{0:00}", - int.Parse(m.Groups[1].Value))); - } - - torrentInfos.Add(release); - } - - return torrentInfos.ToArray(); - } - - public Action, DateTime?> CookiesUpdater { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/BakaBT.cs b/src/NzbDrone.Core/Indexers/Definitions/BakaBT.cs index ecfe03140..e80e35743 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BakaBT.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BakaBT.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.Definitions return new BakaBTParser(Settings, Capabilities.Categories); } - public override async Task Download(Uri link) + public override async Task Download(Uri link) { var request = new HttpRequestBuilder(link.ToString()) .SetCookies(GetCookies() ?? new Dictionary()) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BeyondHD.cs b/src/NzbDrone.Core/Indexers/Definitions/BeyondHD.cs index 80d2856bb..3fc4a3328 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BeyondHD.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BeyondHD.cs @@ -45,10 +45,17 @@ namespace NzbDrone.Core.Indexers.Definitions public override IParseIndexerResponse GetParser() { - return new BeyondHDParser(Capabilities.Categories); + return new BeyondHDParser(Settings, Capabilities.Categories); } - private IndexerCapabilities SetCapabilities() + protected override IList CleanupReleases(IEnumerable releases, SearchCriteriaBase searchCriteria) + { + var cleanReleases = base.CleanupReleases(releases, searchCriteria); + + return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); + } + + private static IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities { @@ -62,7 +69,8 @@ namespace NzbDrone.Core.Indexers.Definitions }, Flags = new List { - IndexerFlag.Internal + IndexerFlag.Internal, + IndexerFlag.Exclusive, } }; @@ -84,7 +92,7 @@ namespace NzbDrone.Core.Indexers.Definitions _capabilities = capabilities; } - private IEnumerable GetPagedRequests(SearchCriteriaBase searchCriteria, string term, string imdbId = null, int tmdbId = 0) + private IEnumerable GetPagedRequests(SearchCriteriaBase searchCriteria, string searchTerm, string imdbId = null, int tmdbId = 0) { var body = new Dictionary { @@ -121,9 +129,9 @@ namespace NzbDrone.Core.Indexers.Definitions body.Add("tmdb_id", tmdbId); } - if (term.IsNotNullOrWhiteSpace()) + if (searchTerm.IsNotNullOrWhiteSpace()) { - body.Add("search", term); + body.Add("search", searchTerm.Trim()); } var cats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories); @@ -191,7 +199,16 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(searchCriteria, searchCriteria.SanitizedTvSearchString, searchCriteria.FullImdbId)); + var searchTerm = searchCriteria.SanitizedTvSearchString; + + if (searchCriteria.Season is > 0 && + searchCriteria.Episode.IsNotNullOrWhiteSpace() && + DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) + { + searchTerm = $"{searchCriteria.SanitizedSearchTerm} {showDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; + } + + pageableRequests.Add(GetPagedRequests(searchCriteria, searchTerm, searchCriteria.FullImdbId)); return pageableRequests; } @@ -220,10 +237,12 @@ namespace NzbDrone.Core.Indexers.Definitions public class BeyondHDParser : IParseIndexerResponse { + private readonly BeyondHDSettings _settings; private readonly IndexerCapabilitiesCategories _categories; - public BeyondHDParser(IndexerCapabilitiesCategories categories) + public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories) { + _settings = settings; _categories = categories; } @@ -257,20 +276,15 @@ namespace NzbDrone.Core.Indexers.Definitions foreach (var row in jsonResponse.Results) { + // Skip invalid results when freeleech or limited filtering is set + if ((_settings.FreeleechOnly && !row.Freeleech) || (_settings.LimitedOnly && !row.Limited)) + { + continue; + } + var details = row.InfoUrl; var link = row.DownloadLink; - // BHD can return crazy values for tmdb - var tmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.TmdbId.Split("/")[1], out var tmdbResult) ? tmdbResult : 0; - var imdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault(); - - var flags = new HashSet(); - - if (row.Internal) - { - flags.Add(IndexerFlag.Internal); - } - var release = new TorrentInfo { Title = row.Name, @@ -280,19 +294,27 @@ namespace NzbDrone.Core.Indexers.Definitions Guid = details, Categories = _categories.MapTrackerCatDescToNewznab(row.Category), PublishDate = DateTime.Parse(row.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), - IndexerFlags = flags, + IndexerFlags = GetIndexerFlags(row), Size = row.Size, Grabs = row.Grabs, Seeders = row.Seeders, - ImdbId = imdbId, - TmdbId = tmdbId, + ImdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault(), Peers = row.Leechers + row.Seeders, DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1, UploadVolumeFactor = 1, MinimumRatio = 1, MinimumSeedTime = 172800, // 120 hours + Languages = row.Audios?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Subs = row.Subtitles?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), }; + // BHD can return crazy values for tmdb + if (row.TmdbId.IsNotNullOrWhiteSpace()) + { + var tmdbId = row.TmdbId.Split("/").ElementAtOrDefault(1); + release.TmdbId = tmdbId != null && ParseUtil.TryCoerceInt(tmdbId, out var tmdbResult) ? tmdbResult : 0; + } + releaseInfos.Add(release); } @@ -302,6 +324,23 @@ namespace NzbDrone.Core.Indexers.Definitions .ToArray(); } + private static HashSet GetIndexerFlags(BeyondHDTorrent item) + { + var flags = new HashSet(); + + if (item.Internal) + { + flags.Add(IndexerFlag.Internal); + } + + if (item.Exclusive) + { + flags.Add(IndexerFlag.Exclusive); + } + + return flags; + } + public Action, DateTime?> CookiesUpdater { get; set; } } @@ -309,8 +348,8 @@ namespace NzbDrone.Core.Indexers.Definitions { public BeyondHDSettingsValidator() { - RuleFor(c => c.ApiKey).NotEmpty(); - RuleFor(c => c.RssKey).NotEmpty(); + RuleFor(c => c.ApiKey).NotEmpty().Length(32); + RuleFor(c => c.RssKey).NotEmpty().Length(32); } } @@ -327,25 +366,25 @@ namespace NzbDrone.Core.Indexers.Definitions SearchTypes = Array.Empty(); } - [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in My Security => API Key)", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerBeyondHDSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "RSS Key", HelpText = "RSS Key from the Site (Found in My Security => RSS Key)", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "IndexerSettingsRssKey", HelpText = "IndexerBeyondHDSettingsRssKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string RssKey { get; set; } - [FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")] + [FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerBeyondHDSettingsFreeleechOnlyHelpText")] public bool FreeleechOnly { get; set; } - [FieldDefinition(5, Label = "Limited Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only (Limited UL)")] + [FieldDefinition(5, Label = "IndexerBeyondHDSettingsLimitedOnly", Type = FieldType.Checkbox, HelpText = "IndexerBeyondHDSettingsLimitedOnlyHelpText")] public bool LimitedOnly { get; set; } - [FieldDefinition(6, Label = "Refund Only", Type = FieldType.Checkbox, HelpText = "Search refund only")] + [FieldDefinition(6, Label = "IndexerBeyondHDSettingsRefundOnly", Type = FieldType.Checkbox, HelpText = "IndexerBeyondHDSettingsRefundOnlyHelpText")] public bool RefundOnly { get; set; } - [FieldDefinition(7, Label = "Rewind Only", Type = FieldType.Checkbox, HelpText = "Search rewind only")] + [FieldDefinition(7, Label = "IndexerBeyondHDSettingsRewindOnly", Type = FieldType.Checkbox, HelpText = "IndexerBeyondHDSettingsRewindOnlyHelpText")] public bool RewindOnly { get; set; } - [FieldDefinition(8, Label = "Search Types", Type = FieldType.TagSelect, SelectOptions = typeof(BeyondHDSearchType), Advanced = true, HelpText = "Select the types of releases that you are interested in. If none selected, all options are used.")] + [FieldDefinition(8, Label = "IndexerBeyondHDSettingsSearchTypes", Type = FieldType.Select, SelectOptions = typeof(BeyondHDSearchType), HelpText = "IndexerBeyondHDSettingsSearchTypesHelpText", Advanced = true)] public IEnumerable SearchTypes { get; set; } public override NzbDroneValidationResult Validate() @@ -433,9 +472,13 @@ namespace NzbDrone.Core.Indexers.Definitions [JsonPropertyName("times_completed")] public int Grabs { get; set; } + public int Seeders { get; set; } public int Leechers { get; set; } + public string Audios { get; set; } + public string Subtitles { get; set; } + [JsonPropertyName("created_at")] public string CreatedAt { get; set; } @@ -461,6 +504,8 @@ namespace NzbDrone.Core.Indexers.Definitions public bool Limited { get; set; } + public bool Exclusive { get; set; } + public bool Internal { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs b/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs index a261f2c71..309da6b26 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs @@ -19,6 +19,7 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Definitions { + [Obsolete("Site has shutdown")] public class BinSearch : UsenetIndexerBase { public override string Name => "BinSearch"; diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNet.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNet.cs index 074e3ec43..9a2e802d9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNet.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNet.cs @@ -9,7 +9,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet public class BroadcastheNet : TorrentIndexerBase { public override string Name => "BroadcasTheNet"; - + public override string[] IndexerUrls => new[] { "https://api.broadcasthe.net/" }; + public override string[] LegacyUrls => new[] { "http://api.broadcasthe.net/" }; + public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows"; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override bool SupportsRss => true; public override bool SupportsSearch => true; @@ -18,11 +20,6 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet public override IndexerCapabilities Capabilities => SetCapabilities(); public override TimeSpan RateLimit => TimeSpan.FromSeconds(5); - public override string[] IndexerUrls => new string[] { "https://api.broadcasthe.net/" }; - public override string[] LegacyUrls => new string[] { "http://api.broadcasthe.net/" }; - - public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows"; - public BroadcastheNet(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) : base(httpClient, eventAggregator, indexerStatusService, configService, logger) { @@ -30,18 +27,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet public override IIndexerRequestGenerator GetRequestGenerator() { - var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, Capabilities = Capabilities }; - - var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id); - if (releaseInfo != null) - { - if (int.TryParse(releaseInfo.Guid.Replace("BTN-", string.Empty), out var torrentId)) - { - requestGenerator.LastRecentTorrentID = torrentId; - } - } - - return requestGenerator; + return new BroadcastheNetRequestGenerator { Settings = Settings, Capabilities = Capabilities, PageSize = PageSize }; } public override IParseIndexerResponse GetParser() @@ -65,7 +51,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet caps.Categories.AddCategoryMapping("720p", NewznabStandardCategory.TVHD, "720p"); caps.Categories.AddCategoryMapping("1080p", NewznabStandardCategory.TVHD, "1080p"); caps.Categories.AddCategoryMapping("1080i", NewznabStandardCategory.TVHD, "1080i"); - caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVHD, "2160p"); + caps.Categories.AddCategoryMapping("2160p", NewznabStandardCategory.TVUHD, "2160p"); caps.Categories.AddCategoryMapping("Portable Device", NewznabStandardCategory.TVSD, "Portable Device"); return caps; diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetParser.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetParser.cs index 4650671c5..60c2c0a58 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetParser.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet public IList ParseResponse(IndexerResponse indexerResponse) { - var results = new List(); + var releaseInfos = new List(); var indexerHttpResponse = indexerResponse.HttpResponse; switch (indexerHttpResponse.StatusCode) @@ -69,65 +69,82 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet if (jsonResponse.Result.Results == 0 || jsonResponse.Result?.Torrents?.Values == null) { - return results; + return releaseInfos; } var protocol = indexerResponse.HttpRequest.Url.Scheme + ":"; foreach (var torrent in jsonResponse.Result.Torrents.Values) { - var torrentInfo = new TorrentInfo(); + var flags = new HashSet(); - torrentInfo.Guid = string.Format("BTN-{0}", torrent.TorrentID); - torrentInfo.Title = CleanReleaseName(torrent.ReleaseName); - torrentInfo.Size = torrent.Size; - torrentInfo.DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol); - torrentInfo.InfoUrl = string.Format("{0}//broadcasthe.net/torrents.php?id={1}&torrentid={2}", protocol, torrent.GroupID, torrent.TorrentID); - - //torrentInfo.CommentUrl = - if (torrent.TvdbID.HasValue) + if (torrent.Origin.ToUpperInvariant() == "INTERNAL") { - torrentInfo.TvdbId = torrent.TvdbID.Value; + flags.Add(IndexerFlag.Internal); } - if (torrent.TvrageID.HasValue) + var releaseInfo = new TorrentInfo { - torrentInfo.TvRageId = torrent.TvrageID.Value; + Guid = $"BTN-{torrent.TorrentID}", + InfoUrl = $"{protocol}//broadcasthe.net/torrents.php?id={torrent.GroupID}&torrentid={torrent.TorrentID}", + DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol), + Title = GetTitle(torrent), + Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution), + InfoHash = torrent.InfoHash, + Size = torrent.Size, + Grabs = torrent.Snatched, + Seeders = torrent.Seeders, + Peers = torrent.Leechers + torrent.Seeders, + PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time), + Origin = torrent.Origin, + Source = torrent.Source, + Container = torrent.Container, + Codec = torrent.Codec, + Resolution = torrent.Resolution, + Scene = torrent.Origin.ToUpperInvariant() == "SCENE", + IndexerFlags = flags, + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1, + MinimumRatio = 1, + MinimumSeedTime = torrent.Category.ToUpperInvariant() == "SEASON" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes + }; + + if (torrent.TvdbID is > 0) + { + releaseInfo.TvdbId = torrent.TvdbID.Value; } - torrentInfo.PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time); + if (torrent.TvrageID is > 0) + { + releaseInfo.TvRageId = torrent.TvrageID.Value; + } - //torrentInfo.MagnetUrl = - torrentInfo.InfoHash = torrent.InfoHash; - torrentInfo.Seeders = torrent.Seeders; - torrentInfo.Peers = torrent.Leechers + torrent.Seeders; - - torrentInfo.Origin = torrent.Origin; - torrentInfo.Source = torrent.Source; - torrentInfo.Container = torrent.Container; - torrentInfo.Codec = torrent.Codec; - torrentInfo.Resolution = torrent.Resolution; - torrentInfo.UploadVolumeFactor = 1; - torrentInfo.DownloadVolumeFactor = 0; - torrentInfo.MinimumRatio = 1; - - torrentInfo.Categories = _categories.MapTrackerCatToNewznab(torrent.Resolution); + if (torrent.ImdbID.IsNotNullOrWhiteSpace() && int.TryParse(torrent.ImdbID, out var imdbId)) + { + releaseInfo.ImdbId = imdbId; + } // Default to TV if category could not be mapped - if (torrentInfo.Categories == null || !torrentInfo.Categories.Any()) + if (releaseInfo.Categories == null || !releaseInfo.Categories.Any()) { - torrentInfo.Categories = new List { NewznabStandardCategory.TV }; + releaseInfo.Categories = new List { NewznabStandardCategory.TV }; } - results.Add(torrentInfo); + releaseInfos.Add(releaseInfo); } - return results; + return releaseInfos; } - private string CleanReleaseName(string releaseName) + private static string GetTitle(BroadcastheNetTorrent torrent) { - releaseName = releaseName.Replace("\\", ""); + var releaseName = torrent.ReleaseName.Replace("\\", ""); + + if (torrent.Container.ToUpperInvariant() is "M2TS" or "ISO") + { + releaseName = Regex.Replace(releaseName, @"\b(H\.?265)\b", "HEVC", RegexOptions.Compiled); + releaseName = Regex.Replace(releaseName, @"\b(H\.?264)\b", "AVC", RegexOptions.Compiled); + } return releaseName; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs index 17fc12a44..9b6d93499 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetRequestGenerator.cs @@ -9,31 +9,13 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet { public class BroadcastheNetRequestGenerator : IIndexerRequestGenerator { - public int MaxPages { get; set; } - public int PageSize { get; set; } public BroadcastheNetSettings Settings { get; set; } public IndexerCapabilities Capabilities { get; set; } - - public int? LastRecentTorrentID { get; set; } + public int PageSize { get; set; } public Func> GetCookies { get; set; } public Action, DateTime?> CookiesUpdater { get; set; } - public BroadcastheNetRequestGenerator() - { - MaxPages = 10; - PageSize = 100; - } - - private IEnumerable GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset) - { - var builder = new JsonRpcRequestBuilder(Settings.BaseUrl) - .Call("getTorrents", Settings.ApiKey, parameters, results, offset); - builder.SuppressHttpError = true; - - yield return new IndexerRequest(builder.Build()); - } - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); @@ -50,7 +32,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet var parameters = new BroadcastheNetTorrentQuery(); - var searchString = searchCriteria.SearchTerm ?? string.Empty; + var searchTerm = searchCriteria.SearchTerm ?? string.Empty; var btnResults = searchCriteria.Limit.GetValueOrDefault(); if (btnResults == 0) @@ -68,9 +50,10 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet { parameters.Tvrage = $"{searchCriteria.RId}"; } - else if (searchString.IsNotNullOrWhiteSpace()) + + if (searchTerm.IsNotNullOrWhiteSpace()) { - parameters.Search = searchString.Replace(" ", "%"); + parameters.Search = searchTerm.Replace(" ", "%"); } // If only the season/episode is searched for then change format to match expected format @@ -91,17 +74,22 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet else if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) { // Daily Episode - parameters.Name = showDate.ToString("yyyy.MM.dd"); + parameters.Name = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture); parameters.Category = "Episode"; pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset)); } 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 = $"S{searchCriteria.Season:00}E{episode:00}%"; parameters.Category = "Episode"; pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset)); } + else if (searchTerm.IsNotNullOrWhiteSpace() && int.TryParse(searchTerm, out _) && (searchCriteria.TvdbId > 0 || searchCriteria.RId > 0)) + { + // Disable ID-based searches for episodes with absolute episode number + return new IndexerPageableRequestChain(); + } else { // Neither a season only search nor daily nor standard, fall back to query @@ -122,7 +110,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet var parameters = new BroadcastheNetTorrentQuery(); - var searchString = searchCriteria.SearchTerm ?? ""; + var searchTerm = searchCriteria.SearchTerm ?? string.Empty; var btnResults = searchCriteria.Limit.GetValueOrDefault(); if (btnResults == 0) @@ -132,11 +120,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet var btnOffset = searchCriteria.Offset.GetValueOrDefault(0); - parameters.Search = searchString.Replace(" ", "%"); + if (searchTerm.IsNotNullOrWhiteSpace()) + { + parameters.Search = searchTerm.Replace(" ", "%"); + } pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset)); return pageableRequests; } + + private IEnumerable GetPagedRequests(BroadcastheNetTorrentQuery parameters, int results, int offset) + { + var builder = new JsonRpcRequestBuilder(Settings.BaseUrl) + .Call("getTorrents", Settings.ApiKey, parameters, results, offset); + + builder.SuppressHttpError = true; + + yield return new IndexerRequest(builder.Build()); + } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetSettings.cs index e52b96888..968f623a8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetSettings.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet BaseSettings.LimitsUnit = (int)IndexerLimitsUnit.Hour; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetTorrentQuery.cs b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetTorrentQuery.cs index 1180f9b63..5cfadcef3 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetTorrentQuery.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BroadcastheNet/BroadcastheNetTorrentQuery.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json; namespace NzbDrone.Core.Indexers.BroadcastheNet { @@ -13,15 +14,15 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Search { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Codec { get; set; } + public IEnumerable Codec { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Container { get; set; } + public IEnumerable Container { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Source { get; set; } + public IEnumerable Source { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Resolution { get; set; } + public IEnumerable Resolution { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Origin { get; set; } + public IEnumerable Origin { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Hash { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs index 9f6bf21a4..80c70c068 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/Cardigann.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann private readonly ICached _generatorCache; public override string Name => "Cardigann"; - public override string[] IndexerUrls => new string[] { "" }; + public override string[] IndexerUrls => new[] { "" }; public override string Description => ""; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; @@ -58,10 +58,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann Settings = Settings }); - generator = (CardigannRequestGenerator)SetCookieFunctions(generator); - + generator.Definition = Definition; generator.Settings = Settings; + generator = (CardigannRequestGenerator)SetCookieFunctions(generator); + _generatorCache.ClearExpired(); return generator; @@ -83,7 +84,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann if (_definitionService.GetCachedDefinition(Settings.DefinitionFile).Search?.Rows?.Filters?.Any(x => x.Name == "andmatch") ?? false) { - cleanReleases = FilterReleasesByQuery(releases, searchCriteria).ToList(); + cleanReleases = FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); } return cleanReleases; diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs index d61f413d7..6abdd710e 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannBase.cs @@ -139,20 +139,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann { var selectorSelector = ApplyGoTemplateText(selector.Selector, variables); - if (dom.Matches(selectorSelector)) - { - selection = dom; - } - else - { - selection = QuerySelector(dom, selectorSelector); - } + selection = dom.Matches(selectorSelector) ? dom : QuerySelector(dom, selectorSelector); if (selection == null) { if (required) { - throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selectorSelector, dom.ToHtmlPretty())); + throw new Exception($"Selector \"{selectorSelector}\" didn't match {dom.ToHtmlPretty()}"); } return null; @@ -195,7 +188,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann { if (required) { - throw new Exception(string.Format("Attribute \"{0}\" is not set for element {1}", selector.Attribute, selection.ToHtmlPretty())); + throw new Exception($"Attribute \"{selector.Attribute}\" is not set for element {selection.ToHtmlPretty()}"); } return null; @@ -337,9 +330,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann variables[name] = selected.Key; break; case "info": - variables[name] = value; - break; + case "info_cookie": + case "info_flaresolverr": + case "info_useragent": + case "info_category_8000": case "cardigannCaptcha": + // no-op break; default: throw new NotSupportedException($"Type {setting.Type} is not supported."); @@ -771,13 +767,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann protected Dictionary ParseCustomHeaders(Dictionary> customHeaders, Dictionary variables) { + var headers = new Dictionary(); + if (customHeaders == null) { - return null; + return headers; } // FIXME: fix jackett header handling (allow it to specifiy the same header multipe times) - var headers = new Dictionary(); foreach (var header in customHeaders) { headers.Add(header.Key, ApplyGoTemplateText(header.Value[0], variables)); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs index 78c2b2760..28be1c3e5 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannParser.cs @@ -39,22 +39,22 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann var indexerLogging = _configService.LogIndexerResponse; - if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl.IsNotNullOrWhiteSpace()) { - if (indexerResponse.HttpResponse.HasHttpRedirect) + _logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl); + + if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php")) { - _logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl); - - if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php")) - { - // Remove cookie cache - CookiesUpdater(null, null); - throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer."); - } - - throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request"); + // Remove cookie cache + CookiesUpdater(null, null); + throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer."); } + throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request"); + } + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); } @@ -502,36 +502,54 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann value = release.Description; break; case "category": + if (fieldModifiers.Contains("noappend")) + { + _logger.Warn("The \"noappend\" modifier is deprecated. Please switch to \"default\". See the Definition Format in the Wiki for more information."); + } + var cats = _categories.MapTrackerCatToNewznab(value); + if (cats.Any()) { - if (release.Categories == null || fieldModifiers.Contains("noappend")) - { - release.Categories = cats; - } - else - { - release.Categories = release.Categories.Union(cats).ToList(); - } + release.Categories = release.Categories == null || fieldModifiers.Contains("noappend") + ? cats + : release.Categories.Union(cats).ToList(); + } + + if (value.IsNotNullOrWhiteSpace() && !release.Categories.Any()) + { + _logger.Warn("[{0}] Invalid category for value: '{1}'", _definition.Id, value); + } + else + { + value = release.Categories.ToString(); } - value = release.Categories.ToString(); break; case "categorydesc": - var catsDesc = _categories.MapTrackerCatDescToNewznab(value); - if (catsDesc.Any()) + if (fieldModifiers.Contains("noappend")) { - if (release.Categories == null || fieldModifiers.Contains("noappend")) - { - release.Categories = catsDesc; - } - else - { - release.Categories = release.Categories.Union(catsDesc).ToList(); - } + _logger.Warn("The \"noappend\" modifier is deprecated. Please switch to \"default\". See the Definition Format in the Wiki for more information."); + } + + var catsDesc = _categories.MapTrackerCatDescToNewznab(value); + + if (catsDesc.Any()) + { + release.Categories = release.Categories == null || fieldModifiers.Contains("noappend") + ? catsDesc + : release.Categories.Union(catsDesc).ToList(); + } + + if (value.IsNotNullOrWhiteSpace() && !release.Categories.Any()) + { + _logger.Warn("[{0}] Invalid category for value: '{1}'", _definition.Id, value); + } + else + { + value = release.Categories.ToString(); } - value = release.Categories.ToString(); break; case "size": release.Size = ParseUtil.GetBytes(value); diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs index 72cccca19..11c8060ab 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannRequestGenerator.cs @@ -212,7 +212,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann } } - var loginUrl = ResolvePath(login.Path).ToString(); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString(); CookiesUpdater(null, null); @@ -253,7 +253,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann } else if (login.Method == "form") { - var loginUrl = ResolvePath(login.Path).ToString(); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)).ToString(); var queryCollection = new NameValueCollection(); var pairs = new Dictionary(); @@ -332,37 +332,47 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann } // selector inputs - if (login.Selectorinputs != null) + if (login.Selectorinputs != null && login.Selectorinputs.Any()) { - foreach (var selectorinput in login.Selectorinputs) + foreach (var selectorInput in login.Selectorinputs) { - string value = null; try { - value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild); - pairs[selectorinput.Key] = value; + var value = HandleSelector(selectorInput.Value, landingResultDocument.FirstElementChild, required: !selectorInput.Value.Optional); + + if (selectorInput.Value.Optional && value == null) + { + continue; + } + + pairs[selectorInput.Key] = value; } catch (Exception ex) { - throw new CardigannException(string.Format("Error while parsing selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message)); + throw new CardigannException($"Error while parsing selector input={selectorInput.Key}, selector={selectorInput.Value.Selector}: {ex.Message}", ex); } } } // getselector inputs - if (login.Getselectorinputs != null) + if (login.Getselectorinputs != null && login.Getselectorinputs.Any()) { - foreach (var selectorinput in login.Getselectorinputs) + foreach (var selectorInput in login.Getselectorinputs) { - string value = null; try { - value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild); - queryCollection[selectorinput.Key] = value; + var value = HandleSelector(selectorInput.Value, landingResultDocument.FirstElementChild, required: !selectorInput.Value.Optional); + + if (selectorInput.Value.Optional && value == null) + { + continue; + } + + queryCollection[selectorInput.Key] = value; } catch (Exception ex) { - throw new CardigannException(string.Format("Error while parsing get selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message)); + throw new CardigannException($"Error while parsing get selector input={selectorInput.Key}, selector={selectorInput.Value.Selector}: {ex.Message}", ex); } } } @@ -524,7 +534,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann } } - var loginUrl = ResolvePath(login.Path + "?" + queryCollection.GetQueryString()).ToString(); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + "?" + queryCollection.GetQueryString()).ToString(); CookiesUpdater(null, null); @@ -553,7 +563,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann else if (login.Method == "oneurl") { var oneUrl = ApplyGoTemplateText(login.Inputs["oneurl"]); - var loginUrl = ResolvePath(login.Path + oneUrl).ToString(); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables) + oneUrl).ToString(); CookiesUpdater(null, null); @@ -629,7 +639,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann var variables = GetBaseTemplateVariables(); var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables); - var loginUrl = ResolvePath(login.Path); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)); var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri) { @@ -690,7 +700,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann var captchaElement = landingResultDocument.QuerySelector(captcha.Selector); if (captchaElement != null) { - var loginUrl = ResolvePath(login.Path); + var loginUrl = ResolvePath(ApplyGoTemplateText(login.Path, variables)); var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl); var request = new HttpRequestBuilder(captchaUrl.ToString()) @@ -1178,14 +1188,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann if (method == HttpMethod.Get && searchUrls.Contains(searchUrl)) { - _logger.Trace("Skip duplicated request {0}", searchUrl); + _logger.Trace("Skip duplicated request for {0}: {1}", Definition.Name, searchUrl); continue; } searchUrls.Add(searchUrl); - _logger.Debug($"Adding request: {searchUrl}"); + _logger.Debug("Adding request for {0}: {1}", Definition.Name, searchUrl); var requestBuilder = new HttpRequestBuilder(searchUrl) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannSettings.cs index 0e6912ad2..51d352649 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Cardigann/CardigannSettings.cs @@ -1,11 +1,23 @@ using System.Collections.Generic; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Indexers.Settings; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Definitions.Cardigann { + public class CardigannSettingsValidator : NoAuthSettingsValidator + { + public CardigannSettingsValidator() + { + RuleFor(c => c.DefinitionFile).NotEmpty(); + } + } + public class CardigannSettings : NoAuthTorrentBaseSettings { + private static readonly CardigannSettingsValidator Validator = new (); + public CardigannSettings() { ExtraFieldData = new Dictionary(); @@ -15,5 +27,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Cardigann public string DefinitionFile { get; set; } public Dictionary ExtraFieldData { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/ExoticaZ.cs b/src/NzbDrone.Core/Indexers/Definitions/ExoticaZ.cs index 784cc56e4..0933ea6a2 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/ExoticaZ.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/ExoticaZ.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Indexers.Definitions { private readonly IndexerCapabilitiesCategories _categories; - protected override string TimezoneOffset => "+01:00"; + protected override string TimezoneOffset => "+02:00"; public ExoticaZParser(IndexerCapabilitiesCategories categories) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileList.cs b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileList.cs index 8367a2db0..ca34098a8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileList.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileList.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Linq; using NLog; using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Definitions.FileList; @@ -40,6 +43,13 @@ public class FileList : TorrentIndexerBase return new FileListParser(Settings, Capabilities.Categories); } + protected override IList CleanupReleases(IEnumerable releases, SearchCriteriaBase searchCriteria) + { + var cleanReleases = base.CleanupReleases(releases, searchCriteria); + + return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); + } + private IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities @@ -93,6 +103,8 @@ public class FileList : TorrentIndexerBase caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.Movies3D, "Filme 3D"); caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.MoviesBluRay, "Filme 4K Blu-Ray"); caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TVUHD, "Seriale 4K"); + caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.MoviesForeign, "RO Dubbed"); + caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVForeign, "RO Dubbed"); return caps; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListApi.cs b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListApi.cs index 6f1fdc12e..8b1e4253a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListApi.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListApi.cs @@ -38,3 +38,8 @@ public class FileListTorrent [JsonPropertyName("small_description")] public string SmallDescription { get; set; } } + +public class FileListErrorResponse +{ + public string Error { get; set; } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListParser.cs index 9c43e639f..6d91d930d 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListParser.cs @@ -28,9 +28,14 @@ public class FileListParser : IParseIndexerResponse throw new IndexerException(indexerResponse, "Unexpected response status {0} code from indexer request", indexerResponse.HttpResponse.StatusCode); } + if (indexerResponse.Content.StartsWith("{\"error\"") && STJson.TryDeserialize(indexerResponse.Content, out var errorResponse)) + { + throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", errorResponse.Error); + } + if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) { - throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); + throw new IndexerException(indexerResponse, "Unexpected response header {0} from indexer request, expected {1}", indexerResponse.HttpResponse.Headers.ContentType, HttpAccept.Json.Value); } var releaseInfos = new List(); @@ -56,7 +61,7 @@ public class FileListParser : IParseIndexerResponse var imdbId = 0; if (row.ImdbId is { Length: > 2 }) { - imdbId = int.Parse(row.ImdbId.Substring(2)); + int.TryParse(row.ImdbId.TrimStart('t'), out imdbId); } var downloadVolumeFactor = row.FreeLeech ? 0 : 1; @@ -72,7 +77,7 @@ public class FileListParser : IParseIndexerResponse InfoUrl = GetInfoUrl(id), Seeders = row.Seeders, Peers = row.Leechers + row.Seeders, - PublishDate = DateTime.Parse(row.UploadDate + " +0200", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), + PublishDate = DateTime.Parse(row.UploadDate + " +0300", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), Description = row.SmallDescription, Genres = row.SmallDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(), ImdbId = imdbId, @@ -96,7 +101,7 @@ public class FileListParser : IParseIndexerResponse var url = new HttpUri(_settings.BaseUrl) .CombinePath("/download.php") .AddQueryParam("id", torrentId.ToString()) - .AddQueryParam("passkey", _settings.Passkey); + .AddQueryParam("passkey", _settings.Passkey.Trim()); return url.FullUri; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListRequestGenerator.cs index 08937d679..ccbe2c356 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListRequestGenerator.cs @@ -151,7 +151,7 @@ public class FileListRequestGenerator : IIndexerRequestGenerator if (searchCriteria.Categories != null && searchCriteria.Categories.Any()) { - parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories))); + parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList())); } if (Settings.FreeleechOnly) diff --git a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListSettings.cs index 5fdcc4744..4183bc8c9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/FileList/FileListSettings.cs @@ -24,13 +24,13 @@ public class FileListSettings : NoAuthTorrentBaseSettings BaseSettings.LimitsUnit = (int)IndexerLimitsUnit.Hour; } - [FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)] + [FieldDefinition(2, Label = "Username", HelpText = "IndexerFileListSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] + [FieldDefinition(3, Label = "IndexerSettingsPasskey", HelpText = "IndexerFileListSettingsPasskeyHelpText", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] public string Passkey { get; set; } - [FieldDefinition(4, Label = "Freeleech Only", HelpText = "Search Freeleech torrents only", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerFileListSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox)] public bool FreeleechOnly { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleApi.cs b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleApi.cs index 0703a00d2..89df33663 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleApi.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleApi.cs @@ -95,3 +95,8 @@ public class GazelleIndexResponse public string Authkey { get; set; } public string Passkey { get; set; } } + +public class GazelleErrorResponse +{ + public string Error { get; init; } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleBase.cs index 711b2e152..584efd1e5 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleBase.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; @@ -82,28 +83,30 @@ public abstract class GazelleBase : TorrentIndexerBase protected virtual bool CheckForLoginError(HttpResponse response) => true; - public override async Task Download(Uri link) + public override async Task Download(Uri link) { - var response = await base.Download(link); + var downloadResponse = await base.Download(link); - if (response.Length >= 1 - && response[0] != 'd' // simple test for torrent vs HTML content + var fileData = downloadResponse.Data; + + if (Settings.UseFreeleechToken == (int)GazelleFreeleechTokenAction.Preferred + && fileData.Length >= 1 + && fileData[0] != 'd' // simple test for torrent vs HTML content && link.Query.Contains("usetoken=1")) { - var html = Encoding.GetString(response); + var html = Encoding.GetString(fileData); + if (html.Contains("You do not have any freeleech tokens left.") || html.Contains("You do not have enough freeleech tokens") || html.Contains("This torrent is too large.") || html.Contains("You cannot use tokens here")) { - // download again without usetoken=1 - var requestLinkNew = link.ToString().Replace("&usetoken=1", ""); - - response = await base.Download(new Uri(requestLinkNew)); + // Try to download again without usetoken + downloadResponse = await base.Download(link.RemoveQueryParam("usetoken")); } } - return response; + return downloadResponse; } protected override IDictionary GetCookies() diff --git a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleParser.cs index 998ba835b..1682dea35 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleParser.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -32,7 +33,9 @@ public class GazelleParser : IParseIndexerResponse // Remove cookie cache CookiesUpdater(null, null); - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize(indexerResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) @@ -59,6 +62,14 @@ public class GazelleParser : IParseIndexerResponse { foreach (var torrent in result.Torrents) { + var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech; + + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (Settings.UseFreeleechToken == (int)GazelleFreeleechTokenAction.Required && !torrent.CanUseToken && !isFreeLeech) + { + continue; + } + var id = torrent.TorrentId; var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]"; @@ -72,20 +83,20 @@ public class GazelleParser : IParseIndexerResponse var release = new TorrentInfo { Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech), Title = WebUtility.HtmlDecode(title), Container = torrent.Encoding, Files = torrent.FileCount, Grabs = torrent.Snatches, Codec = torrent.Format, Size = long.Parse(torrent.Size), - DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken), - InfoUrl = infoUrl, Seeders = int.Parse(torrent.Seeders), Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), PublishDate = torrent.Time.ToUniversalTime(), Scene = torrent.Scene, PosterUrl = posterUrl, - DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1 }; @@ -104,6 +115,14 @@ public class GazelleParser : IParseIndexerResponse } else { + var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech; + + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (Settings.UseFreeleechToken == (int)GazelleFreeleechTokenAction.Required && !result.CanUseToken && !isFreeLeech) + { + continue; + } + var id = result.TorrentId; var groupName = WebUtility.HtmlDecode(result.GroupName); var infoUrl = GetInfoUrl(result.GroupId, id); @@ -111,17 +130,17 @@ public class GazelleParser : IParseIndexerResponse var release = new TorrentInfo { Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech), Title = groupName, Size = long.Parse(result.Size), - DownloadUrl = GetDownloadUrl(id, result.CanUseToken), - InfoUrl = infoUrl, Seeders = int.Parse(result.Seeders), Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders), Files = result.FileCount, Grabs = result.Snatches, PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime((string)result.GroupTime), PosterUrl = posterUrl, - DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1 }; @@ -153,7 +172,7 @@ public class GazelleParser : IParseIndexerResponse .AddQueryParam("action", "download") .AddQueryParam("id", torrentId); - if (Settings.UseFreeleechToken) + if (Settings.UseFreeleechToken is (int)GazelleFreeleechTokenAction.Preferred or (int)GazelleFreeleechTokenAction.Required && canUseToken) { url = url.AddQueryParam("usetoken", "1"); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleSettings.cs index 64a394a77..628436549 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Gazelle/GazelleSettings.cs @@ -16,11 +16,23 @@ public class GazelleSettings : UserPassTorrentBaseSettings public string AuthKey { get; set; } public string PassKey { get; set; } - [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use freeleech tokens when available")] - public bool UseFreeleechToken { get; set; } + [FieldDefinition(5, Type = FieldType.Select, Label = "Use Freeleech Tokens", SelectOptions = typeof(GazelleFreeleechTokenAction), HelpText = "When to use freeleech tokens")] + public int UseFreeleechToken { get; set; } public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } + +public enum GazelleFreeleechTokenAction +{ + [FieldOption(Label = "Never", Hint = "Do not use tokens")] + Never = 0, + + [FieldOption(Label = "Preferred", Hint = "Use token if possible")] + Preferred = 1, + + [FieldOption(Label = "Required", Hint = "Abort download if unable to use token")] + Required = 2, +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs b/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs index 4abc47005..55d52bace 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; @@ -16,6 +20,7 @@ using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -24,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGames : TorrentIndexerBase { public override string Name => "GazelleGames"; - public override string[] IndexerUrls => new string[] { "https://gazellegames.net/" }; + public override string[] IndexerUrls => new[] { "https://gazellegames.net/" }; public override string Description => "GazelleGames (GGn) is a Private Torrent Tracker for GAMES"; public override string Language => "en-US"; public override Encoding Encoding => Encoding.UTF8; @@ -38,7 +43,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new GazelleGamesRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient }; + return new GazelleGamesRequestGenerator(Settings, Capabilities); } public override IParseIndexerResponse GetParser() @@ -48,14 +53,13 @@ namespace NzbDrone.Core.Indexers.Definitions private IndexerCapabilities SetCapabilities() { - var caps = new IndexerCapabilities - { - }; + var caps = new IndexerCapabilities(); // Apple caps.Categories.AddCategoryMapping("Mac", NewznabStandardCategory.ConsoleOther, "Mac"); caps.Categories.AddCategoryMapping("iOS", NewznabStandardCategory.PCMobileiOS, "iOS"); caps.Categories.AddCategoryMapping("Apple Bandai Pippin", NewznabStandardCategory.ConsoleOther, "Apple Bandai Pippin"); + caps.Categories.AddCategoryMapping("Apple II", NewznabStandardCategory.ConsoleOther, "Apple II"); // Google caps.Categories.AddCategoryMapping("Android", NewznabStandardCategory.PCMobileAndroid, "Android"); @@ -78,6 +82,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping("Nintendo GameCube", NewznabStandardCategory.ConsoleOther, "Nintendo GameCube"); caps.Categories.AddCategoryMapping("Pokemon Mini", NewznabStandardCategory.ConsoleOther, "Pokemon Mini"); caps.Categories.AddCategoryMapping("SNES", NewznabStandardCategory.ConsoleOther, "SNES"); + caps.Categories.AddCategoryMapping("Switch", NewznabStandardCategory.ConsoleOther, "Switch"); caps.Categories.AddCategoryMapping("Virtual Boy", NewznabStandardCategory.ConsoleOther, "Virtual Boy"); caps.Categories.AddCategoryMapping("Wii", NewznabStandardCategory.ConsoleWii, "Wii"); caps.Categories.AddCategoryMapping("Wii U", NewznabStandardCategory.ConsoleWiiU, "Wii U"); @@ -110,6 +115,24 @@ namespace NzbDrone.Core.Indexers.Definitions // Amstrad caps.Categories.AddCategoryMapping("Amstrad CPC", NewznabStandardCategory.ConsoleOther, "Amstrad CPC"); + // Bandai + caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan"); + caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color"); + + // Commodore + caps.Categories.AddCategoryMapping("Commodore 64", NewznabStandardCategory.ConsoleOther, "Commodore 64"); + caps.Categories.AddCategoryMapping("Commodore 128", NewznabStandardCategory.ConsoleOther, "Commodore 128"); + caps.Categories.AddCategoryMapping("Commodore Amiga", NewznabStandardCategory.ConsoleOther, "Commodore Amiga"); + caps.Categories.AddCategoryMapping("Amiga CD32", NewznabStandardCategory.ConsoleOther, "Amiga CD32"); + caps.Categories.AddCategoryMapping("Commodore Plus-4", NewznabStandardCategory.ConsoleOther, "Commodore Plus-4"); + caps.Categories.AddCategoryMapping("Commodore VIC-20", NewznabStandardCategory.ConsoleOther, "Commodore VIC-20"); + + // NEC + caps.Categories.AddCategoryMapping("NEC PC-98", NewznabStandardCategory.ConsoleOther, "NEC PC-98"); + caps.Categories.AddCategoryMapping("NEC PC-FX", NewznabStandardCategory.ConsoleOther, "NEC PC-FX"); + caps.Categories.AddCategoryMapping("NEC SuperGrafx", NewznabStandardCategory.ConsoleOther, "NEC SuperGrafx"); + caps.Categories.AddCategoryMapping("NEC TurboGrafx-16", NewznabStandardCategory.ConsoleOther, "NEC TurboGrafx-16"); + // Sinclair caps.Categories.AddCategoryMapping("ZX Spectrum", NewznabStandardCategory.ConsoleOther, "ZX Spectrum"); @@ -133,16 +156,9 @@ namespace NzbDrone.Core.Indexers.Definitions // Other caps.Categories.AddCategoryMapping("3DO", NewznabStandardCategory.ConsoleOther, "3DO"); - caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan"); - caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color"); caps.Categories.AddCategoryMapping("Casio Loopy", NewznabStandardCategory.ConsoleOther, "Casio Loopy"); caps.Categories.AddCategoryMapping("Casio PV-1000", NewznabStandardCategory.ConsoleOther, "Casio PV-1000"); caps.Categories.AddCategoryMapping("Colecovision", NewznabStandardCategory.ConsoleOther, "Colecovision"); - caps.Categories.AddCategoryMapping("Commodore 64", NewznabStandardCategory.ConsoleOther, "Commodore 64"); - caps.Categories.AddCategoryMapping("Commodore 128", NewznabStandardCategory.ConsoleOther, "Commodore 128"); - caps.Categories.AddCategoryMapping("Commodore Amiga", NewznabStandardCategory.ConsoleOther, "Commodore Amiga"); - caps.Categories.AddCategoryMapping("Commodore Plus-4", NewznabStandardCategory.ConsoleOther, "Commodore Plus-4"); - caps.Categories.AddCategoryMapping("Commodore VIC-20", NewznabStandardCategory.ConsoleOther, "Commodore VIC-20"); caps.Categories.AddCategoryMapping("Emerson Arcadia 2001", NewznabStandardCategory.ConsoleOther, "Emerson Arcadia 2001"); caps.Categories.AddCategoryMapping("Entex Adventure Vision", NewznabStandardCategory.ConsoleOther, "Entex Adventure Vision"); caps.Categories.AddCategoryMapping("Epoch Super Casette Vision", NewznabStandardCategory.ConsoleOther, "Epoch Super Casette Vision"); @@ -157,13 +173,11 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping("Mattel Intellivision", NewznabStandardCategory.ConsoleOther, "Mattel Intellivision"); caps.Categories.AddCategoryMapping("Memotech MTX", NewznabStandardCategory.ConsoleOther, "Memotech MTX"); caps.Categories.AddCategoryMapping("Miles Gordon Sam Coupe", NewznabStandardCategory.ConsoleOther, "Miles Gordon Sam Coupe"); - caps.Categories.AddCategoryMapping("NEC PC-98", NewznabStandardCategory.ConsoleOther, "NEC PC-98"); - caps.Categories.AddCategoryMapping("NEC PC-FX", NewznabStandardCategory.ConsoleOther, "NEC PC-FX"); - caps.Categories.AddCategoryMapping("NEC SuperGrafx", NewznabStandardCategory.ConsoleOther, "NEC SuperGrafx"); - caps.Categories.AddCategoryMapping("NEC TurboGrafx-16", NewznabStandardCategory.ConsoleOther, "NEC TurboGrafx-16"); caps.Categories.AddCategoryMapping("Nokia N-Gage", NewznabStandardCategory.ConsoleOther, "Nokia N-Gage"); + caps.Categories.AddCategoryMapping("Oculus Quest", NewznabStandardCategory.ConsoleOther, "Oculus Quest"); caps.Categories.AddCategoryMapping("Ouya", NewznabStandardCategory.ConsoleOther, "Ouya"); caps.Categories.AddCategoryMapping("Philips Videopac+", NewznabStandardCategory.ConsoleOther, "Philips Videopac+"); + caps.Categories.AddCategoryMapping("Philips CD-i", NewznabStandardCategory.ConsoleOther, "Philips CD-i"); caps.Categories.AddCategoryMapping("Phone/PDA", NewznabStandardCategory.ConsoleOther, "Phone/PDA"); caps.Categories.AddCategoryMapping("RCA Studio II", NewznabStandardCategory.ConsoleOther, "RCA Studio II"); caps.Categories.AddCategoryMapping("Sharp X1", NewznabStandardCategory.ConsoleOther, "Sharp X1"); @@ -178,31 +192,62 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping("Retro - Other", NewznabStandardCategory.ConsoleOther, "Retro - Other"); // special categories (real categories/not platforms) - caps.Categories.AddCategoryMapping("OST", NewznabStandardCategory.AudioOther, "OST"); - caps.Categories.AddCategoryMapping("Applications", NewznabStandardCategory.PC0day, "Applications"); - caps.Categories.AddCategoryMapping("E-Books", NewznabStandardCategory.BooksEBook, "E-Books"); + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC0day, "Applications"); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook, "E-Books"); + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioOther, "OST"); return caps; } protected override async Task Test(List failures) { - ((GazelleGamesRequestGenerator)GetRequestGenerator()).FetchPasskey(); - await base.Test(failures); + await FetchPasskey().ConfigureAwait(false); + + await base.Test(failures).ConfigureAwait(false); + } + + private async Task FetchPasskey() + { + var request = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}/api.php") + .Accept(HttpAccept.Json) + .SetHeader("X-API-Key", Settings.Apikey) + .AddQueryParam("request", "quick_user") + .Build(); + + var indexResponse = await _httpClient.ExecuteProxiedAsync(request, Definition).ConfigureAwait(false); + + var index = Json.Deserialize(indexResponse.Content); + + if (index == null || + string.IsNullOrWhiteSpace(index.Status) || + index.Status != "success" || + string.IsNullOrWhiteSpace(index.Response.PassKey)) + { + throw new IndexerAuthException("Failed to authenticate with GazelleGames."); + } + + // Set passkey on settings so it can be used to generate the download URL + Settings.Passkey = index.Response.PassKey; } } public class GazelleGamesRequestGenerator : IIndexerRequestGenerator { - public GazelleGamesSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - public IIndexerHttpClient HttpClient { get; set; } + private readonly GazelleGamesSettings _settings; + private readonly IndexerCapabilities _capabilities; + + public GazelleGamesRequestGenerator(GazelleGamesSettings settings, IndexerCapabilities capabilities) + { + _settings = settings; + _capabilities = capabilities; + } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -211,7 +256,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -220,7 +265,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -229,7 +274,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -238,61 +283,77 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } - public void FetchPasskey() + private IEnumerable GetRequest(List> parameters) { - // GET on index for the passkey - var request = RequestBuilder().Resource("api.php?request=quick_user").Build(); - var indexResponse = HttpClient.Execute(request); - var index = Json.Deserialize(indexResponse.Content); - if (index == null || - string.IsNullOrWhiteSpace(index.Status) || - index.Status != "success" || - string.IsNullOrWhiteSpace(index.Response.PassKey)) - { - throw new Exception("Failed to authenticate with GazelleGames."); - } - - // Set passkey on settings so it can be used to generate the download URL - Settings.Passkey = index.Response.PassKey; - } - - private IEnumerable GetRequest(string parameters) - { - var req = RequestBuilder() - .Resource($"api.php?{parameters}") + var request = RequestBuilder() + .Resource($"/api.php?{parameters.GetQueryString()}") .Build(); - yield return new IndexerRequest(req); + yield return new IndexerRequest(request); } private HttpRequestBuilder RequestBuilder() { - return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}") + return new HttpRequestBuilder($"{_settings.BaseUrl.Trim().TrimEnd('/')}") + .Resource("/api.php") .Accept(HttpAccept.Json) - .SetHeader("X-API-Key", Settings.Apikey); + .SetHeader("X-API-Key", _settings.Apikey); } - private string GetBasicSearchParameters(string searchTerm, int[] categories) + private List> GetBasicSearchParameters(string searchTerm, SearchCriteriaBase searchCriteria) { - var parameters = "request=search&search_type=torrents&empty_groups=filled&order_by=time&order_way=desc"; - - if (!string.IsNullOrWhiteSpace(searchTerm)) + var parameters = new List> { - var searchType = Settings.SearchGroupNames ? "groupname" : "searchstr"; + { "request", "search" }, + { "search_type", "torrents" }, + { "empty_groups", "filled" }, + { "order_by", "time" }, + { "order_way", "desc" } + }; - parameters += string.Format("&{1}={0}", searchTerm.Replace(".", " "), searchType); + if (searchTerm.IsNotNullOrWhiteSpace()) + { + parameters.Add( + _settings.SearchGroupNames ? "groupname" : "searchstr", + searchTerm.Replace(".", " ")); } - if (categories != null) + if (searchCriteria.Categories != null) { - foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories)) + var categoryMappings = _capabilities.Categories + .MapTorznabCapsToTrackers(searchCriteria.Categories) + .Distinct() + .Where(x => !x.IsAllDigits()) + .ToList(); + + categoryMappings.ForEach(category => parameters.Add("artistcheck[]", category)); + } + + if (_settings.FreeleechOnly) + { + parameters.Add("freetorrent", "1"); + } + + if (searchCriteria.MinSize is > 0) + { + var minSize = searchCriteria.MinSize.Value / 1024L / 1024L; + if (minSize > 0) { - parameters += string.Format("&artistcheck[]={0}", cat); + parameters.Add("sizesmall", minSize.ToString()); + } + } + + if (searchCriteria.MaxSize is > 0) + { + var maxSize = searchCriteria.MaxSize.Value / 1024L / 1024L; + if (maxSize > 0) + { + parameters.Add("sizeslarge", maxSize.ToString()); } } @@ -308,6 +369,8 @@ namespace NzbDrone.Core.Indexers.Definitions private readonly GazelleGamesSettings _settings; private readonly IndexerCapabilitiesCategories _categories; + private static Regex YearRegex => new (@"\b(?:19|20|21)\d{2}\b", RegexOptions.Compiled); + public GazelleGamesParser(GazelleGamesSettings settings, IndexerCapabilitiesCategories categories) { _settings = settings; @@ -329,66 +392,72 @@ namespace NzbDrone.Core.Indexers.Definitions } var jsonResponse = new HttpResponse(indexerResponse.HttpResponse); + if (jsonResponse.Resource.Status != "success" || string.IsNullOrWhiteSpace(jsonResponse.Resource.Status) || - jsonResponse.Resource.Response == null) + jsonResponse.Resource.Response is not JObject response) { return torrentInfos; } - Dictionary response; + var groups = response.ToObject>(JsonSerializer.Create(Json.GetSerializerSettings())); - try + foreach (var group in groups) { - response = ((JObject)jsonResponse.Resource.Response).ToObject>(); - } - catch - { - return torrentInfos; - } - - foreach (var result in response) - { - Dictionary torrents; - - try - { - torrents = ((JObject)result.Value.Torrents).ToObject>(); - } - catch + if (group.Value.Torrents is not JObject groupTorrents) { continue; } - if (result.Value.Torrents != null) + var torrents = groupTorrents + .ToObject>(JsonSerializer.Create(Json.GetSerializerSettings())) + .Where(t => t.Value.TorrentType.ToUpperInvariant() == "TORRENT") + .ToList(); + + var categories = group.Value.Artists + .SelectMany(a => _categories.MapTrackerCatDescToNewznab(a.Name)) + .Distinct() + .ToArray(); + + foreach (var torrent in torrents) { - var categories = result.Value.Artists.Select(a => a.Name); + Enum.TryParse(torrent.Value.FreeTorrent, true, out GazelleGamesFreeTorrent freeTorrent); + var downloadVolumeFactor = freeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1; - foreach (var torrent in torrents) + // Skip non-freeleech results when freeleech only is set + if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0) { - var id = int.Parse(torrent.Key); - - var infoUrl = GetInfoUrl(result.Key, id); - - var release = new TorrentInfo() - { - Guid = infoUrl, - Title = torrent.Value.ReleaseTitle, - Files = torrent.Value.FileCount, - Grabs = torrent.Value.Snatched, - Size = long.Parse(torrent.Value.Size), - DownloadUrl = GetDownloadUrl(id), - InfoUrl = infoUrl, - Seeders = torrent.Value.Seeders, - Categories = _categories.MapTrackerCatDescToNewznab(categories.FirstOrDefault()), - Peers = torrent.Value.Leechers + torrent.Value.Seeders, - PublishDate = torrent.Value.Time.ToUniversalTime(), - DownloadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.FreeLeech || torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1, - UploadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1 - }; - - torrentInfos.Add(release); + continue; } + + var torrentId = torrent.Key; + var infoUrl = GetInfoUrl(group.Key, torrentId); + + if (categories.Length == 0) + { + categories = _categories.MapTrackerCatToNewznab(torrent.Value.CategoryId.ToString()).ToArray(); + } + + var release = new TorrentInfo + { + Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(torrentId), + Title = GetTitle(group.Value, torrent.Value), + Categories = categories, + Files = torrent.Value.FileCount, + Size = long.Parse(torrent.Value.Size), + Grabs = torrent.Value.Snatched, + Seeders = torrent.Value.Seeders, + Peers = torrent.Value.Leechers + torrent.Value.Seeders, + PublishDate = torrent.Value.Time.ToUniversalTime(), + Scene = torrent.Value.Scene == 1, + DownloadVolumeFactor = downloadVolumeFactor, + UploadVolumeFactor = freeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1, + MinimumSeedTime = 288000 // Minimum of 3 days and 8 hours (80 hours in total) + }; + + torrentInfos.Add(release); } } @@ -399,6 +468,54 @@ namespace NzbDrone.Core.Indexers.Definitions .ToArray(); } + private static string GetTitle(GazelleGamesGroup group, GazelleGamesTorrent torrent) + { + var title = WebUtility.HtmlDecode(torrent.ReleaseTitle); + + if (group.Year is > 0 && title.IsNotNullOrWhiteSpace() && !YearRegex.Match(title).Success) + { + title += $" ({group.Year})"; + } + + if (torrent.RemasterTitle.IsNotNullOrWhiteSpace()) + { + title += $" [{$"{WebUtility.HtmlDecode(torrent.RemasterTitle)} {torrent.RemasterYear}".Trim()}]"; + } + + var flags = new List + { + $"{torrent.Format} {torrent.Encoding}".Trim() + }; + + if (group.Artists is { Count: > 0 }) + { + flags.AddIfNotNull(group.Artists.Select(a => a.Name).Join(", ")); + } + + flags.AddIfNotNull(torrent.Language); + flags.AddIfNotNull(torrent.Region); + flags.AddIfNotNull(torrent.Miscellaneous); + + if (torrent.Dupable == 1) + { + flags.Add("Trumpable"); + } + + flags = flags.Where(x => x.IsNotNullOrWhiteSpace()).ToList(); + + if (flags.Any()) + { + title += $" [{string.Join(" / ", flags)}]"; + } + + if (torrent.GameDoxType.IsNotNullOrWhiteSpace()) + { + title += $" [{torrent.GameDoxType.Trim()}]"; + } + + return title; + } + private string GetDownloadUrl(int torrentId) { // AuthKey is required but not checked, just pass in a dummy variable @@ -413,7 +530,7 @@ namespace NzbDrone.Core.Indexers.Definitions return url.FullUri; } - private string GetInfoUrl(string groupId, int torrentId) + private string GetInfoUrl(int groupId, int torrentId) { var url = new HttpUri(_settings.BaseUrl) .CombinePath("/torrents.php") @@ -444,12 +561,15 @@ namespace NzbDrone.Core.Indexers.Definitions Passkey = ""; } - [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings), Must have User Permissions", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerGazelleGamesSettingsApiKeyHelpText", HelpTextWarning = "IndexerGazelleGamesSettingsApiKeyHelpTextWarning", Privacy = PrivacyLevel.ApiKey)] public string Apikey { get; set; } - [FieldDefinition(3, Label = "Search Group Names", Type = FieldType.Checkbox, HelpText = "Search Group Names Only")] + [FieldDefinition(3, Label = "IndexerGazelleGamesSettingsSearchGroupNames", Type = FieldType.Checkbox, HelpText = "IndexerGazelleGamesSettingsSearchGroupNamesHelpText")] public bool SearchGroupNames { get; set; } + [FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerGazelleGamesSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox, Advanced = true)] + public bool FreeleechOnly { get; set; } + public string Passkey { get; set; } public override NzbDroneValidationResult Validate() @@ -466,8 +586,9 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGamesGroup { - public List Artists { get; set; } + public ReadOnlyCollection Artists { get; set; } public object Torrents { get; set; } + public int? Year { get; set; } } public class GazelleGamesArtist @@ -478,16 +599,30 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGamesTorrent { + public int CategoryId { get; set; } + public string Format { get; set; } + public string Encoding { get; set; } + public string Language { get; set; } + public string Region { get; set; } + public string RemasterYear { get; set; } + public string RemasterTitle { get; set; } + public string ReleaseTitle { get; set; } + public string Miscellaneous { get; set; } + public int Scene { get; set; } + public int Dupable { get; set; } + public DateTime Time { get; set; } + public string TorrentType { get; set; } + public int FileCount { get; set; } public string Size { get; set; } public int? Snatched { get; set; } public int Seeders { get; set; } public int Leechers { get; set; } - public string ReleaseTitle { get; set; } - public DateTime Time { get; set; } - public int FileCount { get; set; } - public GazelleGamesFreeTorrent FreeTorrent { get; set; } + public string FreeTorrent { get; set; } public bool PersonalFL { get; set; } public bool LowSeedFL { get; set; } + + [JsonProperty("GameDOXType")] + public string GameDoxType { get; set; } } public class GazelleGamesUserResponse @@ -503,9 +638,9 @@ namespace NzbDrone.Core.Indexers.Definitions public enum GazelleGamesFreeTorrent { - Normal, - FreeLeech, - Neutral, - Either + Normal = 0, + FreeLeech = 1, + Neutral = 2, + Either = 3 } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/GreatPosterWall.cs b/src/NzbDrone.Core/Indexers/Definitions/GreatPosterWall.cs index 931b2623e..29e01057f 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/GreatPosterWall.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/GreatPosterWall.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Definitions.Gazelle; @@ -148,7 +149,9 @@ public class GreatPosterWallParser : GazelleParser throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from indexer request"); } - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize(indexerResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) @@ -168,16 +171,24 @@ public class GreatPosterWallParser : GazelleParser { foreach (var torrent in result.Torrents) { + var isFreeLeech = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech; + + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (_settings.UseFreeleechToken == (int)GazelleFreeleechTokenAction.Required && !torrent.CanUseToken && !isFreeLeech) + { + continue; + } + var infoUrl = GetInfoUrl(result.GroupId.ToString(), torrent.TorrentId); var time = DateTime.SpecifyKind(torrent.Time, DateTimeKind.Unspecified); var release = new TorrentInfo { - Title = WebUtility.HtmlDecode(torrent.FileName).Trim(), Guid = infoUrl, InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(torrent.TorrentId, torrent.CanUseToken && !isFreeLeech), + Title = WebUtility.HtmlDecode(torrent.FileName).Trim(), PosterUrl = GetPosterUrl(result.Cover), - DownloadUrl = GetDownloadUrl(torrent.TorrentId, torrent.CanUseToken), PublishDate = new DateTimeOffset(time, TimeSpan.FromHours(8)).UtcDateTime, // Time is Chinese Time, add 8 hours difference from UTC Categories = ParseCategories(torrent), Size = torrent.Size, @@ -186,7 +197,7 @@ public class GreatPosterWallParser : GazelleParser Grabs = torrent.Snatches, Files = torrent.FileCount, Scene = torrent.Scene, - DownloadVolumeFactor = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1, MinimumRatio = 1, MinimumSeedTime = 172800 // 48 hours @@ -234,7 +245,7 @@ public class GreatPosterWallParser : GazelleParser .AddQueryParam("action", "download") .AddQueryParam("id", torrentId); - if (_settings.UseFreeleechToken && canUseToken) + if (_settings.UseFreeleechToken is (int)GazelleFreeleechTokenAction.Preferred or (int)GazelleFreeleechTokenAction.Required && canUseToken) { url = url.AddQueryParam("usetoken", "1"); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBits.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBits.cs index 84ea1be25..b4d924511 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBits.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBits.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits return new HDBitsParser(Settings, Capabilities.Categories); } - private IndexerCapabilities SetCapabilities() + private static IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities { @@ -43,6 +43,11 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits MovieSearchParams = new List { MovieSearchParam.Q, MovieSearchParam.ImdbId + }, + Flags = new List + { + IndexerFlag.Internal, + IndexerFlag.Exclusive, } }; diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsApi.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsApi.cs index 4d607beb0..5f8b947ae 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsApi.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsApi.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits public IEnumerable Category { get; set; } public IEnumerable Codec { get; set; } public IEnumerable Medium { get; set; } - public int? Origin { get; set; } + public IEnumerable Origin { get; set; } [JsonProperty(PropertyName = "imdb")] public ImdbInfo ImdbInfo { get; set; } @@ -85,6 +85,9 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits [JsonProperty(PropertyName = "type_origin")] public int TypeOrigin { get; set; } + [JsonProperty(PropertyName = "type_exclusive")] + public int TypeExclusive { get; set; } + [JsonProperty(PropertyName = "imdb")] public ImdbInfo ImdbInfo { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsInfo.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsInfo.cs deleted file mode 100644 index 412295abf..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions.HDBits -{ - public class HDBitsInfo : TorrentInfo - { - public bool? Internal { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsParser.cs index b157aaf9b..8b0b55319 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsParser.cs @@ -62,16 +62,8 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits } var id = result.Id; - var internalRelease = result.TypeOrigin == 1; - var flags = new HashSet(); - - if (internalRelease) - { - flags.Add(IndexerFlag.Internal); - } - - releaseInfos.Add(new HDBitsInfo + releaseInfos.Add(new TorrentInfo { Guid = $"HDBits-{id}", Title = GetTitle(result), @@ -85,28 +77,43 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits Files = (int)result.NumFiles, Peers = result.Leechers + result.Seeders, PublishDate = result.Added.ToUniversalTime(), - Internal = internalRelease, Year = result.ImdbInfo?.Year ?? 0, ImdbId = result.ImdbInfo?.Id ?? 0, TvdbId = result.TvdbInfo?.Id ?? 0, DownloadVolumeFactor = GetDownloadVolumeFactor(result), UploadVolumeFactor = GetUploadVolumeFactor(result), - IndexerFlags = flags + IndexerFlags = GetIndexerFlags(result) }); } return releaseInfos.ToArray(); } - public Action, DateTime?> CookiesUpdater { get; set; } - private string GetTitle(TorrentQueryResponse item) { - return _settings.UseFilenames && item.FileName.IsNotNullOrWhiteSpace() + // Use release name for XXX content and full discs + return item.TypeCategory != 7 && item.TypeMedium != 1 && _settings.UseFilenames && item.FileName.IsNotNullOrWhiteSpace() ? item.FileName.Replace(".torrent", "", StringComparison.InvariantCultureIgnoreCase) : item.Name; } + private static HashSet GetIndexerFlags(TorrentQueryResponse item) + { + var flags = new HashSet(); + + if (item.TypeOrigin == 1) + { + flags.Add(IndexerFlag.Internal); + } + + if (item.TypeExclusive == 1) + { + flags.Add(IndexerFlag.Exclusive); + } + + return flags; + } + private double GetDownloadVolumeFactor(TorrentQueryResponse item) { if (item.FreeLeech == "yes") @@ -153,5 +160,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits return url.FullUri; } + + public Action, DateTime?> CookiesUpdater { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsRequestGenerator.cs index 0307c19f3..87e24b1c2 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsRequestGenerator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Text.RegularExpressions; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -26,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits if (imdbId == 0 && searchCriteria.SearchTerm.IsNotNullOrWhiteSpace()) { - query.Search = searchCriteria.SanitizedSearchTerm; + query.Search = Regex.Replace(searchCriteria.SanitizedSearchTerm, "[\\W]+", " ").Trim(); } if (imdbId != 0) @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) { - query.Search = showDate.ToString("yyyy-MM-dd"); + query.Search = showDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } else { @@ -122,8 +123,20 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits query.Username = Settings.Username; query.Passkey = Settings.ApiKey; - query.Codec = Settings.Codecs.ToArray(); - query.Medium = Settings.Mediums.ToArray(); + if (Settings.Codecs.Any()) + { + query.Codec = Settings.Codecs.ToArray(); + } + + if (Settings.Mediums.Any()) + { + query.Medium = Settings.Mediums.ToArray(); + } + + if (Settings.Origins.Any()) + { + query.Origin = Settings.Origins.ToArray(); + } if (searchCriteria.Categories?.Length > 0) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsSettings.cs index d171a7299..48f65413e 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDBits/HDBitsSettings.cs @@ -24,26 +24,30 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits { Codecs = Array.Empty(); Mediums = Array.Empty(); + Origins = Array.Empty(); FreeleechOnly = false; UseFilenames = true; } - [FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)] + [FieldDefinition(2, Label = "Username", HelpText = "IndexerHDBitsSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "IndexerSettingsPasskey", HelpText = "IndexerHDBitsSettingsPasskeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Codecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "If unspecified, all options are used.")] + [FieldDefinition(4, Label = "IndexerHDBitsSettingsCodecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), HelpText = "IndexerHDBitsSettingsCodecsHelpText", Advanced = true)] public IEnumerable Codecs { get; set; } - [FieldDefinition(5, Label = "Mediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "If unspecified, all options are used.")] + [FieldDefinition(5, Label = "IndexerHDBitsSettingsMediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), HelpText = "IndexerHDBitsSettingsMediumsHelpText", Advanced = true)] public IEnumerable Mediums { get; set; } - [FieldDefinition(6, Label = "Freeleech Only", Type = FieldType.Checkbox, Advanced = true, HelpText = "Show freeleech releases only")] + [FieldDefinition(6, Label = "IndexerHDBitsSettingsOrigins", Type = FieldType.Select, SelectOptions = typeof(HdBitsOrigin), HelpText = "IndexerHDBitsSettingsOriginsHelpText", Advanced = true)] + public IEnumerable Origins { get; set; } + + [FieldDefinition(7, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerHDBitsSettingsFreeleechOnlyHelpText", Advanced = true)] public bool FreeleechOnly { get; set; } - [FieldDefinition(7, Label = "Use Filenames", Type = FieldType.Checkbox, HelpText = "Check this option if you want to use torrent filenames as release titles")] + [FieldDefinition(8, Label = "IndexerHDBitsSettingsUseFilenames", Type = FieldType.Checkbox, HelpText = "IndexerHDBitsSettingsUseFilenamesHelpText")] public bool UseFilenames { get; set; } public override NzbDroneValidationResult Validate() @@ -79,4 +83,12 @@ namespace NzbDrone.Core.Indexers.Definitions.HDBits [FieldOption("WEB-DL")] WebDl = 6 } + + public enum HdBitsOrigin + { + [FieldOption("Undefined")] + Undefined = 0, + [FieldOption("Internal")] + Internal = 1 + } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDSpace.cs b/src/NzbDrone.Core/Indexers/Definitions/HDSpace.cs index 486f69448..8b2933799 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDSpace.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDSpace.cs @@ -20,6 +20,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Definitions { + [Obsolete("Moved to YML for Cardigann")] public class HDSpace : TorrentIndexerBase { public override string Name => "HD-Space"; diff --git a/src/NzbDrone.Core/Indexers/Definitions/HDTorrents.cs b/src/NzbDrone.Core/Indexers/Definitions/HDTorrents.cs index a16db1721..47b01f8cd 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/HDTorrents.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/HDTorrents.cs @@ -338,8 +338,7 @@ namespace NzbDrone.Core.Indexers.Definitions dlVolumeFactor = 0.25; } - var imdbLink = row.QuerySelector("a[href*=\"www.imdb.com/title/\"]")?.GetAttribute("href"); - var imdb = !string.IsNullOrWhiteSpace(imdbLink) ? ParseUtil.GetImdbId(imdbLink) : null; + var imdbId = ParseUtil.GetImdbId(row.QuerySelector("a[href*=\"www.imdb.com/title/\"]")?.GetAttribute("href")?.TrimEnd('/')?.Split('/')?.LastOrDefault()); var flags = new HashSet(); @@ -358,7 +357,7 @@ namespace NzbDrone.Core.Indexers.Definitions PosterUrl = poster, PublishDate = publishDate, Categories = _categories.MapTrackerCatToNewznab(cat), - ImdbId = imdb ?? 0, + ImdbId = imdbId ?? 0, Size = size, Grabs = grabs, Seeders = seeders, diff --git a/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs b/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs index c2e66ae08..f8028c0b8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Headphones/Headphones.cs @@ -50,28 +50,33 @@ namespace NzbDrone.Core.Indexers.Headphones } } - public override async Task Download(Uri link) + public override async Task Download(Uri link) { var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri); - var downloadBytes = Array.Empty(); - var request = requestBuilder.Build(); request.Credentials = new BasicNetworkCredential(Settings.Username, Settings.Password); + byte[] downloadBytes; + long elapsedTime; + try { var response = await _httpClient.ExecuteProxiedAsync(request, Definition); downloadBytes = response.ResponseData; + elapsedTime = response.ElapsedTime; } catch (Exception) { _indexerStatusService.RecordFailure(Definition.Id); _logger.Error("Download failed"); + throw; } - return downloadBytes; + ValidateDownloadData(downloadBytes); + + return new IndexerDownloadResponse(downloadBytes, elapsedTime); } private IndexerCapabilities SetCapabilities() diff --git a/src/NzbDrone.Core/Indexers/Definitions/Headphones/HeadphonesRssParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Headphones/HeadphonesRssParser.cs index 8e0292f3c..94903adb8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Headphones/HeadphonesRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Headphones/HeadphonesRssParser.cs @@ -70,16 +70,17 @@ namespace NzbDrone.Core.Indexers.Headphones protected override bool PostProcess(IndexerResponse indexerResponse, List items, List releases) { var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any()) { - _logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]); - } - else - { - _logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]); + _logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); + + return false; } + + _logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); } return true; diff --git a/src/NzbDrone.Core/Indexers/Definitions/IPTorrents.cs b/src/NzbDrone.Core/Indexers/Definitions/IPTorrents.cs index 5036239f7..d77c26935 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/IPTorrents.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/IPTorrents.cs @@ -73,6 +73,13 @@ namespace NzbDrone.Core.Indexers.Definitions return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); } + protected override IList CleanupReleases(IEnumerable releases, SearchCriteriaBase searchCriteria) + { + var cleanReleases = base.CleanupReleases(releases, searchCriteria); + + return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); + } + private IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities @@ -182,7 +189,7 @@ namespace NzbDrone.Core.Indexers.Definitions var qc = new NameValueCollection(); - foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)) + foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct()) { qc.Add(cat, string.Empty); } @@ -196,10 +203,13 @@ namespace NzbDrone.Core.Indexers.Definitions { // ipt uses sphinx, which supports boolean operators and grouping qc.Add("q", "+(" + imdbId + ")"); + + // search in description + qc.Add("qf", "all"); } - // changed from else if to if to support searching imdbid + season/episode in the same query - if (!string.IsNullOrWhiteSpace(term)) + // changed from "else if" to "if" to support searching imdbid + season/episode in the same query + if (term.IsNotNullOrWhiteSpace()) { // similar to above qc.Add("q", "+(" + term + ")"); @@ -248,7 +258,14 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria, searchCriteria.FullImdbId)); + var searchTerm = $"{searchCriteria.SanitizedTvSearchString}"; + + if (searchCriteria.Season > 0 && searchCriteria.Episode.IsNullOrWhiteSpace()) + { + searchTerm += "*"; + } + + pageableRequests.Add(GetPagedRequests(searchTerm.Trim(), searchCriteria, searchCriteria.FullImdbId)); return pageableRequests; } @@ -293,6 +310,10 @@ namespace NzbDrone.Core.Indexers.Definitions var parser = new HtmlParser(); using var doc = parser.ParseDocument(indexerResponse.Content); + var headerColumns = doc.QuerySelectorAll("table[id=\"torrents\"] > thead > tr > th").Select(x => x.TextContent.Trim()).ToList(); + var sizeIndex = FindColumnIndexOrDefault(headerColumns, "Sort by size", 5); + var filesIndex = FindColumnIndexOrDefault(headerColumns, "Sort by files"); + var rows = doc.QuerySelectorAll("table[id=\"torrents\"] > tbody > tr"); foreach (var row in rows) { @@ -314,9 +335,8 @@ namespace NzbDrone.Core.Indexers.Definitions var dateSplit = descrSplit.Last().Split(new[] { " by " }, StringSplitOptions.None); var publishDate = DateTimeUtil.FromTimeAgo(dateSplit.First()); var description = descrSplit.Length > 1 ? "Tags: " + descrSplit.First().Trim() : ""; - description += dateSplit.Length > 1 ? " Uploaded by: " + dateSplit.Last().Trim() : ""; - var catIcon = row.QuerySelector("td:nth-of-type(1) a"); + var catIcon = row.QuerySelector("td:nth-of-type(1) a[href^=\"?\"]"); if (catIcon == null) { // Torrents - Category column == Text or Code @@ -325,38 +345,42 @@ namespace NzbDrone.Core.Indexers.Definitions } // Torrents - Category column == Icons - var cat = _categories.MapTrackerCatToNewznab(catIcon.GetAttribute("href").Substring(1)); + var cat = _categories.MapTrackerCatToNewznab(catIcon.GetAttribute("href")?.Substring(1)); - var size = ParseUtil.GetBytes(row.Children[5].TextContent); + var size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent); - var colIndex = 6; int? files = null; - if (row.Children.Length == 10) + if (filesIndex != -1) { - files = ParseUtil.CoerceInt(row.Children[colIndex].TextContent.Replace("Go to files", "")); - colIndex++; + files = ParseUtil.CoerceInt(row.Children[filesIndex].TextContent.Replace("Go to files", "")); } - var grabs = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent); - var seeders = ParseUtil.CoerceInt(row.Children[colIndex++].TextContent); - var leechers = ParseUtil.CoerceInt(row.Children[colIndex].TextContent); - var dlVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1; + var colIndex = row.Children.Length == 10 ? 7 : 6; + + var grabsIndex = FindColumnIndexOrDefault(headerColumns, "Sort by snatches", colIndex++); + var seedersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by seeders", colIndex++); + var leechersIndex = FindColumnIndexOrDefault(headerColumns, "Sort by leechers", colIndex); + + var grabs = ParseUtil.CoerceInt(row.Children[grabsIndex].TextContent); + var seeders = ParseUtil.CoerceInt(row.Children[seedersIndex].TextContent); + var leechers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent); var release = new TorrentInfo { - Title = title, Guid = details.AbsoluteUri, DownloadUrl = link.AbsoluteUri, InfoUrl = details.AbsoluteUri, - PublishDate = publishDate, + Title = title, + Description = description, Categories = cat, Size = size, Files = files, Grabs = grabs, Seeders = seeders, Peers = seeders + leechers, - DownloadVolumeFactor = dlVolumeFactor, + PublishDate = publishDate, + DownloadVolumeFactor = row.QuerySelector("span.free") != null ? 0 : 1, UploadVolumeFactor = 1, MinimumRatio = 1, MinimumSeedTime = 1209600 // 336 hours @@ -368,14 +392,24 @@ namespace NzbDrone.Core.Indexers.Definitions return torrentInfos.ToArray(); } + private static int FindColumnIndexOrDefault(List columns, string name, int defaultIndex = -1) + { + var index = columns.FindIndex(x => x.Equals(name, StringComparison.Ordinal)); + + return index != -1 ? index : defaultIndex; + } + public Action, DateTime?> CookiesUpdater { get; set; } private static string CleanTitle(string title) { - // drop invalid chars that seems to have cropped up in some titles. #6582 + // Drop invalid chars that seems to have cropped up in some titles. #6582 title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled); title = Regex.Replace(title, @"[\(\[\{]REQ(UEST(ED)?)?[\)\]\}]", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase); + // Drop languages between brackets conflicting with anime release group parsing + title = Regex.Replace(title, @"^\[[a-z0-9 ._-]+\][-._ ](?.*-[a-z0-9]+)$", "${title}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + return title.Trim(' ', '-', ':'); } } @@ -392,10 +426,10 @@ namespace NzbDrone.Core.Indexers.Definitions { private static readonly IPTorrentsValidator Validator = new (); - [FieldDefinition(3, Label = "Cookie User-Agent", Type = FieldType.Textbox, HelpText = "User-Agent associated with cookie used from Browser")] + [FieldDefinition(3, Label = "IndexerIPTorrentsSettingsCookieUserAgent", Type = FieldType.Textbox, HelpText = "IndexerIPTorrentsSettingsCookieUserAgentHelpText")] public string UserAgent { get; set; } - [FieldDefinition(4, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search FreeLeech torrents only")] + [FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerIPTorrentsSettingsFreeleechOnlyHelpText")] public bool FreeLeechOnly { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/ImmortalSeed.cs b/src/NzbDrone.Core/Indexers/Definitions/ImmortalSeed.cs index d904c4d05..26f5ada43 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/ImmortalSeed.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/ImmortalSeed.cs @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Indexers.Definitions protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) { - return httpResponse.Content.Contains("You do not have permission to access this page."); + return !httpResponse.Content.Contains("logout.php"); } private IndexerCapabilities SetCapabilities() diff --git a/src/NzbDrone.Core/Indexers/Definitions/Knaben.cs b/src/NzbDrone.Core/Indexers/Definitions/Knaben.cs new file mode 100644 index 000000000..286ef1a1a --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Knaben.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Indexers.Settings; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; +using static Newtonsoft.Json.Formatting; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Knaben : TorrentIndexerBase<NoAuthTorrentBaseSettings> + { + public override string Name => "Knaben"; + public override string[] IndexerUrls => new[] { "https://knaben.org/" }; + public override string[] LegacyUrls => new[] { "https://knaben.eu/" }; + public override string Description => "Knaben is a Public torrent meta-search engine"; + public override IndexerPrivacy Privacy => IndexerPrivacy.Public; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Knaben(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new KnabenRequestGenerator(Capabilities); + } + + public override IParseIndexerResponse GetParser() + { + return new KnabenParser(Capabilities.Categories); + } + + private static IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List<TvSearchParam> + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List<MovieSearchParam> + { + MovieSearchParam.Q + }, + MusicSearchParams = new List<MusicSearchParam> + { + MusicSearchParam.Q + }, + BookSearchParams = new List<BookSearchParam> + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping(1000000, NewznabStandardCategory.Audio, "Audio"); + caps.Categories.AddCategoryMapping(1001000, NewznabStandardCategory.AudioMP3, "MP3"); + caps.Categories.AddCategoryMapping(1002000, NewznabStandardCategory.AudioLossless, "Lossless"); + caps.Categories.AddCategoryMapping(1003000, NewznabStandardCategory.AudioAudiobook, "Audiobook"); + caps.Categories.AddCategoryMapping(1004000, NewznabStandardCategory.AudioVideo, "Audio Video"); + caps.Categories.AddCategoryMapping(1005000, NewznabStandardCategory.AudioOther, "Radio"); + caps.Categories.AddCategoryMapping(1006000, NewznabStandardCategory.AudioOther, "Audio Other"); + caps.Categories.AddCategoryMapping(2000000, NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping(2001000, NewznabStandardCategory.TVHD, "TV HD"); + caps.Categories.AddCategoryMapping(2002000, NewznabStandardCategory.TVSD, "TV SD"); + caps.Categories.AddCategoryMapping(2003000, NewznabStandardCategory.TVUHD, "TV UHD"); + caps.Categories.AddCategoryMapping(2004000, NewznabStandardCategory.TVDocumentary, "Documentary"); + caps.Categories.AddCategoryMapping(2005000, NewznabStandardCategory.TVForeign, "TV Foreign"); + caps.Categories.AddCategoryMapping(2006000, NewznabStandardCategory.TVSport, "Sport"); + caps.Categories.AddCategoryMapping(2007000, NewznabStandardCategory.TVOther, "Cartoon"); + caps.Categories.AddCategoryMapping(2008000, NewznabStandardCategory.TVOther, "TV Other"); + caps.Categories.AddCategoryMapping(3000000, NewznabStandardCategory.Movies, "Movies"); + caps.Categories.AddCategoryMapping(3001000, NewznabStandardCategory.MoviesHD, "Movies HD"); + caps.Categories.AddCategoryMapping(3002000, NewznabStandardCategory.MoviesSD, "Movies SD"); + caps.Categories.AddCategoryMapping(3003000, NewznabStandardCategory.MoviesUHD, "Movies UHD"); + caps.Categories.AddCategoryMapping(3004000, NewznabStandardCategory.MoviesDVD, "Movies DVD"); + caps.Categories.AddCategoryMapping(3005000, NewznabStandardCategory.MoviesForeign, "Movies Foreign"); + caps.Categories.AddCategoryMapping(3006000, NewznabStandardCategory.MoviesForeign, "Movies Bollywood"); + caps.Categories.AddCategoryMapping(3007000, NewznabStandardCategory.Movies3D, "Movies 3D"); + caps.Categories.AddCategoryMapping(3008000, NewznabStandardCategory.MoviesOther, "Movies Other"); + caps.Categories.AddCategoryMapping(4000000, NewznabStandardCategory.PC, "PC"); + caps.Categories.AddCategoryMapping(4001000, NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping(4002000, NewznabStandardCategory.PC0day, "Software"); + caps.Categories.AddCategoryMapping(4003000, NewznabStandardCategory.PCMac, "Mac"); + caps.Categories.AddCategoryMapping(4004000, NewznabStandardCategory.PCISO, "Unix"); + caps.Categories.AddCategoryMapping(5000000, NewznabStandardCategory.XXX, "XXX"); + caps.Categories.AddCategoryMapping(5001000, NewznabStandardCategory.XXXx264, "XXX Video"); + caps.Categories.AddCategoryMapping(5002000, NewznabStandardCategory.XXXImageSet, "XXX ImageSet"); + caps.Categories.AddCategoryMapping(5003000, NewznabStandardCategory.XXXOther, "XXX Games"); + caps.Categories.AddCategoryMapping(5004000, NewznabStandardCategory.XXXOther, "XXX Hentai"); + caps.Categories.AddCategoryMapping(5005000, NewznabStandardCategory.XXXOther, "XXX Other"); + caps.Categories.AddCategoryMapping(6000000, NewznabStandardCategory.TVAnime, "Anime"); + caps.Categories.AddCategoryMapping(6001000, NewznabStandardCategory.TVAnime, "Anime Subbed"); + caps.Categories.AddCategoryMapping(6002000, NewznabStandardCategory.TVAnime, "Anime Dubbed"); + caps.Categories.AddCategoryMapping(6003000, NewznabStandardCategory.TVAnime, "Anime Dual audio"); + caps.Categories.AddCategoryMapping(6004000, NewznabStandardCategory.TVAnime, "Anime Raw"); + caps.Categories.AddCategoryMapping(6005000, NewznabStandardCategory.AudioVideo, "Music Video"); + caps.Categories.AddCategoryMapping(6006000, NewznabStandardCategory.BooksOther, "Literature"); + caps.Categories.AddCategoryMapping(6007000, NewznabStandardCategory.AudioOther, "Music"); + caps.Categories.AddCategoryMapping(6008000, NewznabStandardCategory.TVAnime, "Anime non-english translated"); + caps.Categories.AddCategoryMapping(7000000, NewznabStandardCategory.Console, "Console"); + caps.Categories.AddCategoryMapping(7001000, NewznabStandardCategory.ConsolePS4, "PS4"); + caps.Categories.AddCategoryMapping(7002000, NewznabStandardCategory.ConsolePS3, "PS3"); + caps.Categories.AddCategoryMapping(7003000, NewznabStandardCategory.ConsolePS3, "PS2"); + caps.Categories.AddCategoryMapping(7004000, NewznabStandardCategory.ConsolePS3, "PS1"); + caps.Categories.AddCategoryMapping(7005000, NewznabStandardCategory.ConsolePSVita, "PS Vita"); + caps.Categories.AddCategoryMapping(7006000, NewznabStandardCategory.ConsolePSP, "PSP"); + caps.Categories.AddCategoryMapping(7007000, NewznabStandardCategory.ConsoleXBox360, "Xbox 360"); + caps.Categories.AddCategoryMapping(7008000, NewznabStandardCategory.ConsoleXBox, "Xbox"); + caps.Categories.AddCategoryMapping(7009000, NewznabStandardCategory.ConsoleNDS, "Switch"); + caps.Categories.AddCategoryMapping(7010000, NewznabStandardCategory.ConsoleNDS, "NDS"); + caps.Categories.AddCategoryMapping(7011000, NewznabStandardCategory.ConsoleWii, "Wii"); + caps.Categories.AddCategoryMapping(7012000, NewznabStandardCategory.ConsoleWiiU, "WiiU"); + caps.Categories.AddCategoryMapping(7013000, NewznabStandardCategory.Console3DS, "3DS"); + caps.Categories.AddCategoryMapping(7014000, NewznabStandardCategory.ConsoleWii, "GameCube"); + caps.Categories.AddCategoryMapping(7015000, NewznabStandardCategory.ConsoleOther, "Other"); + caps.Categories.AddCategoryMapping(8000000, NewznabStandardCategory.PCMobileOther, "Mobile"); + caps.Categories.AddCategoryMapping(8001000, NewznabStandardCategory.PCMobileAndroid, "Android"); + caps.Categories.AddCategoryMapping(8002000, NewznabStandardCategory.PCMobileiOS, "IOS"); + caps.Categories.AddCategoryMapping(8003000, NewznabStandardCategory.PCMobileOther, "PC Other"); + caps.Categories.AddCategoryMapping(9000000, NewznabStandardCategory.Books, "Books"); + caps.Categories.AddCategoryMapping(9001000, NewznabStandardCategory.BooksEBook, "EBooks"); + caps.Categories.AddCategoryMapping(9002000, NewznabStandardCategory.BooksComics, "Comics"); + caps.Categories.AddCategoryMapping(9003000, NewznabStandardCategory.BooksMags, "Magazines"); + caps.Categories.AddCategoryMapping(9004000, NewznabStandardCategory.BooksTechnical, "Technical"); + caps.Categories.AddCategoryMapping(9005000, NewznabStandardCategory.BooksOther, "Books Other"); + caps.Categories.AddCategoryMapping(10000000, NewznabStandardCategory.Other, "Other"); + caps.Categories.AddCategoryMapping(10001000, NewznabStandardCategory.OtherMisc, "Other Misc"); + + return caps; + } + } + + public class KnabenRequestGenerator : IIndexerRequestGenerator + { + private const string ApiSearchEndpoint = "https://api.knaben.org/v1"; + + private readonly IndexerCapabilities _capabilities; + + public KnabenRequestGenerator(IndexerCapabilities capabilities) + { + _capabilities = capabilities; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedTvSearchString)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(CreateRequest(searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + private IEnumerable<IndexerRequest> CreateRequest(SearchCriteriaBase searchCriteria, string searchTerm) + { + var body = new Dictionary<string, object> + { + { "order_by", "date" }, + { "order_direction", "desc" }, + { "from", 0 }, + { "size", 100 }, + { "hide_unsafe", true } + }; + + var searchQuery = searchTerm.Trim(); + + if (searchQuery.IsNotNullOrWhiteSpace()) + { + body.Add("search_type", "100%"); + body.Add("search_field", "title"); + body.Add("query", searchQuery); + } + + var categories = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories); + + if (categories is { Count: > 0 }) + { + body.Add("categories", categories.Select(int.Parse).Distinct().ToArray()); + } + + var request = new HttpRequest(ApiSearchEndpoint, HttpAccept.Json) + { + Headers = + { + ContentType = "application/json" + }, + Method = HttpMethod.Post + }; + request.SetContent(body.ToJson()); + request.ContentSummary = body.ToJson(None); + + yield return new IndexerRequest(request); + } + + public Func<IDictionary<string, string>> GetCookies { get; set; } + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } + } + + public class KnabenParser : IParseIndexerResponse + { + private static readonly Regex DateTimezoneRegex = new (@"[+-]\d{2}:\d{2}$", RegexOptions.Compiled); + + private readonly IndexerCapabilitiesCategories _categories; + + public KnabenParser(IndexerCapabilitiesCategories categories) + { + _categories = categories; + } + + public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) + { + var indexerHttpResponse = indexerResponse.HttpResponse; + + if (indexerHttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerHttpResponse.StatusCode} code from indexer request"); + } + + if (!indexerHttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + throw new IndexerException(indexerResponse, $"Unexpected response header {indexerHttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); + } + + var releaseInfos = new List<ReleaseInfo>(); + + var jsonResponse = STJson.Deserialize<KnabenResponse>(indexerResponse.Content); + + if (jsonResponse?.Hits == null) + { + return releaseInfos; + } + + var rows = jsonResponse.Hits.Where(r => r.Seeders > 0).ToList(); + + foreach (var row in rows) + { + // Not all entries have the TZ in the "date" field + var publishDate = row.Date.IsNotNullOrWhiteSpace() && !DateTimezoneRegex.IsMatch(row.Date) ? $"{row.Date}+01:00" : row.Date; + + var releaseInfo = new TorrentInfo + { + Guid = row.InfoUrl, + Title = row.Title, + InfoUrl = row.InfoUrl, + DownloadUrl = row.DownloadUrl.IsNotNullOrWhiteSpace() ? row.DownloadUrl : null, + MagnetUrl = row.MagnetUrl.IsNotNullOrWhiteSpace() ? row.MagnetUrl : null, + Categories = row.CategoryIds.SelectMany(cat => _categories.MapTrackerCatToNewznab(cat.ToString())).Distinct().ToList(), + InfoHash = row.InfoHash, + Size = row.Size, + Seeders = row.Seeders, + Peers = row.Leechers + row.Seeders, + PublishDate = DateTime.Parse(publishDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1 + }; + + releaseInfos.Add(releaseInfo); + } + + // order by date + return releaseInfos; + } + + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } + } + + internal sealed class KnabenResponse + { + public IReadOnlyCollection<KnabenRelease> Hits { get; init; } = Array.Empty<KnabenRelease>(); + } + + internal sealed class KnabenRelease + { + public string Title { get; init; } + + [JsonPropertyName("categoryId")] + public IReadOnlyCollection<int> CategoryIds { get; init; } = Array.Empty<int>(); + + [JsonPropertyName("hash")] + public string InfoHash { get; init; } + + [JsonPropertyName("details")] + public string InfoUrl { get; init; } + + [JsonPropertyName("link")] + public string DownloadUrl { get; init; } + + public string MagnetUrl { get; init; } + + [JsonPropertyName("bytes")] + public long Size { get; init; } + + public int Seeders { get; init; } + + [JsonPropertyName("peers")] + public int Leechers { get; init; } + + public string Date { get; init; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs b/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs new file mode 100644 index 000000000..c8f08a87d --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/MTeamTp.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FluentValidation; +using Newtonsoft.Json; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Indexers.Settings; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions; + +public class MTeamTp : TorrentIndexerBase<MTeamTpSettings> +{ + public override string Name => "M-Team - TP"; + public override string[] IndexerUrls => new[] + { + "https://kp.m-team.cc/", + "https://tp.m-team.cc/", + "https://pt.m-team.cc/" + }; + public override string Description => "M-Team TP (MTTP) is a CHINESE Private Torrent Tracker for HD MOVIES / TV / 3X"; + public override string Language => "zh-CN"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override int PageSize => 100; + public override TimeSpan RateLimit => TimeSpan.FromSeconds(5); + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public const string UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.35"; + + public MTeamTp(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new MTeamTpRequestGenerator(Settings, Capabilities, BuildApiUrl(Settings)); + } + + public override IParseIndexerResponse GetParser() + { + return new MTeamTpParser(Settings, Capabilities.Categories, BuildApiUrl(Settings)); + } + + public override async Task<IndexerDownloadResponse> Download(Uri link) + { + var request = new HttpRequestBuilder(link.ToString()) + .SetHeader("x-api-key", Settings.ApiKey) + .Accept(HttpAccept.Json) + .Post() + .Build(); + + request.Headers.UserAgent = UserAgent; + + var response = await _httpClient.ExecuteProxiedAsync(request, Definition); + + if (!STJson.TryDeserialize<MTeamTpApiDownloadTokenResponse>(response.Content, out var jsonResponse)) + { + throw new ReleaseDownloadException("Invalid response received from M-Team, not a valid JSON"); + } + + if (jsonResponse.Data.IsNullOrWhiteSpace()) + { + throw new ReleaseDownloadException($"Unable to find download link for: {link}"); + } + + return await base.Download(new Uri(jsonResponse.Data)); + } + + protected override Task<HttpRequest> GetDownloadRequest(Uri link) + { + var request = new HttpRequest(link.AbsoluteUri) + { + AllowAutoRedirect = true, + Headers = + { + UserAgent = UserAgent + } + }; + + return Task.FromResult(request); + } + + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) + { + return false; + } + + private static IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List<TvSearchParam> + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId + }, + MovieSearchParams = new List<MovieSearchParam> + { + MovieSearchParam.Q, MovieSearchParam.ImdbId + }, + MusicSearchParams = new List<MusicSearchParam> + { + MusicSearchParam.Q + }, + BookSearchParams = new List<BookSearchParam> + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping(401, NewznabStandardCategory.MoviesSD, "Movie(電影)/SD"); + caps.Categories.AddCategoryMapping(419, NewznabStandardCategory.MoviesHD, "Movie(電影)/HD"); + caps.Categories.AddCategoryMapping(420, NewznabStandardCategory.MoviesDVD, "Movie(電影)/DVDiSo"); + caps.Categories.AddCategoryMapping(421, NewznabStandardCategory.MoviesBluRay, "Movie(電影)/Blu-Ray"); + caps.Categories.AddCategoryMapping(439, NewznabStandardCategory.MoviesHD, "Movie(電影)/Remux"); + caps.Categories.AddCategoryMapping(403, NewznabStandardCategory.TVSD, "TV Series(影劇/綜藝)/SD"); + caps.Categories.AddCategoryMapping(402, NewznabStandardCategory.TVHD, "TV Series(影劇/綜藝)/HD"); + caps.Categories.AddCategoryMapping(435, NewznabStandardCategory.TVSD, "TV Series(影劇/綜藝)/DVDiSo"); + caps.Categories.AddCategoryMapping(438, NewznabStandardCategory.TVHD, "TV Series(影劇/綜藝)/BD"); + caps.Categories.AddCategoryMapping(404, NewznabStandardCategory.TVDocumentary, "紀錄教育"); + caps.Categories.AddCategoryMapping(405, NewznabStandardCategory.TVAnime, "Anime(動畫)"); + caps.Categories.AddCategoryMapping(407, NewznabStandardCategory.TVSport, "Sports(運動)"); + caps.Categories.AddCategoryMapping(422, NewznabStandardCategory.PC0day, "Software(軟體)"); + caps.Categories.AddCategoryMapping(423, NewznabStandardCategory.PCGames, "PCGame(PC遊戲)"); + caps.Categories.AddCategoryMapping(427, NewznabStandardCategory.BooksEBook, "Study/Edu ebook(教育書面)"); + caps.Categories.AddCategoryMapping(441, NewznabStandardCategory.BooksOther, "Study/Edu video(教育影片)"); + caps.Categories.AddCategoryMapping(442, NewznabStandardCategory.AudioAudiobook, "Study/Edu audio(教育音檔)"); + caps.Categories.AddCategoryMapping(409, NewznabStandardCategory.Other, "Misc(其他)"); + + // music + caps.Categories.AddCategoryMapping(406, NewznabStandardCategory.AudioVideo, "MV(演唱)"); + caps.Categories.AddCategoryMapping(408, NewznabStandardCategory.AudioOther, "Music(AAC/ALAC)"); + caps.Categories.AddCategoryMapping(434, NewznabStandardCategory.Audio, "Music(無損)"); + + // adult + caps.Categories.AddCategoryMapping(410, NewznabStandardCategory.XXX, "AV(有碼)/HD Censored"); + caps.Categories.AddCategoryMapping(429, NewznabStandardCategory.XXX, "AV(無碼)/HD Uncensored"); + caps.Categories.AddCategoryMapping(424, NewznabStandardCategory.XXXSD, "AV(有碼)/SD Censored"); + caps.Categories.AddCategoryMapping(430, NewznabStandardCategory.XXXSD, "AV(無碼)/SD Uncensored"); + caps.Categories.AddCategoryMapping(426, NewznabStandardCategory.XXXDVD, "AV(無碼)/DVDiSo Uncensored"); + caps.Categories.AddCategoryMapping(437, NewznabStandardCategory.XXXDVD, "AV(有碼)/DVDiSo Censored"); + caps.Categories.AddCategoryMapping(431, NewznabStandardCategory.XXX, "AV(有碼)/Blu-Ray Censored"); + caps.Categories.AddCategoryMapping(432, NewznabStandardCategory.XXX, "AV(無碼)/Blu-Ray Uncensored"); + caps.Categories.AddCategoryMapping(436, NewznabStandardCategory.XXX, "AV(網站)/0Day"); + caps.Categories.AddCategoryMapping(425, NewznabStandardCategory.XXX, "IV(寫真影集)/Video Collection"); + caps.Categories.AddCategoryMapping(433, NewznabStandardCategory.XXXImageSet, "IV(寫真圖集)/Picture Collection"); + caps.Categories.AddCategoryMapping(411, NewznabStandardCategory.XXX, "H-Game(遊戲)"); + caps.Categories.AddCategoryMapping(412, NewznabStandardCategory.XXX, "H-Anime(動畫)"); + caps.Categories.AddCategoryMapping(413, NewznabStandardCategory.XXX, "H-Comic(漫畫)"); + caps.Categories.AddCategoryMapping(440, NewznabStandardCategory.XXX, "AV(Gay)/HD"); + + return caps; + } + + private static string BuildApiUrl(IIndexerSettings settings) + { + return $"https://api.{settings.BaseUrl.AsSpan(settings.BaseUrl.IndexOf('.') + 1)}"; + } +} + +public class MTeamTpRequestGenerator : IIndexerRequestGenerator +{ + private readonly MTeamTpSettings _settings; + private readonly IndexerCapabilities _capabilities; + private readonly string _apiUrl; + + private readonly int[] _trackerAdultCategories = { 410, 429, 424, 430, 426, 437, 431, 432, 436, 425, 433, 411, 412, 413, 440 }; + + public MTeamTpRequestGenerator(MTeamTpSettings settings, IndexerCapabilities capabilities, string apiUrl) + { + _settings = settings; + _capabilities = capabilities; + _apiUrl = apiUrl; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Normal, searchCriteria, searchCriteria.SanitizedSearchTerm, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Normal, searchCriteria, searchCriteria.SanitizedTvSearchString, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Normal, searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Normal, searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Normal, searchCriteria, searchCriteria.SanitizedSearchTerm)); + pageableRequests.Add(GetPagedRequests(MTeamTpRequestType.Adult, searchCriteria, searchCriteria.SanitizedSearchTerm)); + + return pageableRequests; + } + + private IEnumerable<IndexerRequest> GetPagedRequests(MTeamTpRequestType mTeamTpRequestType, SearchCriteriaBase searchCriteria, string searchTerm, string imdbId = null) + { + var categoryMapping = _capabilities.Categories + .MapTorznabCapsToTrackers(searchCriteria.Categories) + .Select(int.Parse) + .Distinct() + .ToList(); + + var adultCategories = categoryMapping.Where(c => _trackerAdultCategories.Contains(c)).ToList(); + var normalCategories = categoryMapping.Except(adultCategories).ToList(); + + switch (mTeamTpRequestType) + { + case MTeamTpRequestType.Adult when adultCategories.Any(): + yield return BuildSearchRequest(mTeamTpRequestType, adultCategories, searchTerm, imdbId); + break; + case MTeamTpRequestType.Normal when !categoryMapping.Any() || normalCategories.Any(): + yield return BuildSearchRequest(mTeamTpRequestType, normalCategories, searchTerm, imdbId); + break; + } + } + + private IndexerRequest BuildSearchRequest(MTeamTpRequestType requestType, IEnumerable<int> categoryMapping, string searchTerm, string imdbId) + { + var request = new HttpRequestBuilder(_apiUrl) + .Resource("/api/torrent/search") + .SetHeader("x-api-key", _settings.ApiKey) + .Accept(HttpAccept.Json) + .Post() + .Build(); + + var query = new MTeamTpApiSearchQuery + { + Mode = requestType, + Categories = categoryMapping?.Select(x => x.ToString()).ToArray() ?? Array.Empty<string>(), + PageNumber = 1, + PageSize = 100 + }; + + if (imdbId.IsNotNullOrWhiteSpace()) + { + query.Imdb = imdbId.Trim(); + } + + if (searchTerm.IsNotNullOrWhiteSpace()) + { + query.Keyword = searchTerm.Trim(); + } + + if (_settings.FreeleechOnly) + { + query.Discount = "FREE"; + } + + request.Headers.ContentType = "application/json"; + request.SetContent(query.ToJson()); + request.ContentSummary = query.ToJson(Formatting.None); + request.Headers.UserAgent = MTeamTp.UserAgent; + + return new IndexerRequest(request); + } + + public Func<IDictionary<string, string>> GetCookies { get; set; } + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } +} + +public class MTeamTpParser : IParseIndexerResponse +{ + private readonly MTeamTpSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + private readonly string _apiUrl; + + public MTeamTpParser(MTeamTpSettings settings, IndexerCapabilitiesCategories categories, string apiUrl) + { + _settings = settings; + _categories = categories; + _apiUrl = apiUrl; + } + + public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) + { + var httpResponse = indexerResponse.HttpResponse; + + if (httpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + } + + if (!httpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); + } + + if (!STJson.TryDeserialize<MTeamTpApiResponse>(indexerResponse.Content, out var jsonResponse)) + { + throw new IndexerException(indexerResponse, "Invalid response received from M-Team, not a valid JSON"); + } + + var releaseInfos = new List<ReleaseInfo>(); + + if (jsonResponse?.Data?.Torrents == null) + { + if (jsonResponse != null && + jsonResponse.Message.IsNotNullOrWhiteSpace() && + jsonResponse.Message.ToUpperInvariant() != "SUCCESS") + { + throw new IndexerException(indexerResponse, $"Invalid response received from M-Team. Response from API: {jsonResponse.Message}"); + } + + return releaseInfos; + } + + foreach (var torrent in jsonResponse.Data.Torrents) + { + var torrentId = int.Parse(torrent.Id); + var infoUrl = $"{_settings.BaseUrl.TrimEnd('/')}/detail/{torrentId}"; + + var release = new TorrentInfo + { + Guid = infoUrl, + Title = CleanTitle(torrent.Name), + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(torrentId), + Categories = _categories.MapTrackerCatToNewznab(torrent.Category), + Description = torrent.Description, + Files = int.Parse(torrent.NumFiles), + Size = long.Parse(torrent.Size), + Grabs = int.Parse(torrent.Status.TimesCompleted), + Seeders = int.Parse(torrent.Status.Seeders), + Peers = int.Parse(torrent.Status.Seeders) + int.Parse(torrent.Status.Leechers), + DownloadVolumeFactor = torrent.Status.Discount.ToUpperInvariant() switch + { + "FREE" => 0, + "_2X_FREE" => 0, + "PERCENT_50" => 0.5, + "_2X_PERCENT_50" => 0.5, + "PERCENT_70" => 0.3, + _ => 1 + }, + UploadVolumeFactor = torrent.Status.Discount.ToUpperInvariant() switch + { + "_2X_FREE" => 2, + "_2X_PERCENT_50" => 2, + _ => 1 + }, + MinimumRatio = 1, + MinimumSeedTime = 172800 // 2 days + }; + + if (torrent.Status?.CreatedDate != null && + DateTime.TryParseExact($"{torrent.Status.CreatedDate} +08:00", "yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var publishDate)) + { + release.PublishDate = publishDate; + } + + releaseInfos.Add(release); + } + + return releaseInfos + .OrderByDescending(o => o.PublishDate) + .ToArray(); + } + + private string GetDownloadUrl(int torrentId) + { + var url = new HttpUri(_apiUrl) + .CombinePath("/api/torrent/genDlToken") + .AddQueryParam("id", torrentId); + + return url.FullUri; + } + + private static string CleanTitle(string title) + { + title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled); + + return title.Trim(); + } + + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } +} + +public class MTeamTpValidator : NoAuthSettingsValidator<MTeamTpSettings> +{ + public MTeamTpValidator() + { + RuleFor(c => c.ApiKey).NotEmpty(); + } +} + +public class MTeamTpSettings : NoAuthTorrentBaseSettings +{ + private static readonly MTeamTpValidator Validator = new (); + + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerMTeamTpSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] + public string ApiKey { get; set; } + + [FieldDefinition(3, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox, HelpText = "IndexerMTeamTpSettingsFreeleechOnlyHelpText")] + public bool FreeleechOnly { get; set; } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } +} + +internal enum MTeamTpRequestType +{ + Normal, + Adult +} + +internal class MTeamTpApiSearchQuery +{ + [JsonProperty(Required = Required.Always)] + public MTeamTpRequestType Mode { get; set; } + + [JsonProperty(Required = Required.Always)] + public IEnumerable<string> Categories { get; set; } + + public string Discount { get; set; } + public string Imdb { get; set; } + public string Keyword { get; set; } + public int? PageNumber { get; set; } + public int? PageSize { get; set; } +} + +internal class MTeamTpApiResponse +{ + public MTeamTpApiData Data { get; set; } + public string Message { get; set; } +} + +internal class MTeamTpApiData +{ + [JsonPropertyName("data")] + public IReadOnlyCollection<MTeamTpApiTorrent> Torrents { get; set; } +} + +internal class MTeamTpApiTorrent +{ + public string Id { get; set; } + public string Name { get; set; } + + [JsonPropertyName("smallDescr")] + public string Description { get; set; } + + public string Category { get; set; } + + [JsonPropertyName("numfiles")] + public string NumFiles { get; set; } + + public string Imdb { get; set; } + public string Size { get; set; } + public MTeamTpApiReleaseStatus Status { get; set; } +} + +internal class MTeamTpApiReleaseStatus +{ + public string CreatedDate { get; set; } + public string Discount { get; set; } + public string TimesCompleted { get; set; } + public string Seeders { get; set; } + public string Leechers { get; set; } +} + +internal class MTeamTpApiDownloadTokenResponse +{ + public string Data { get; set; } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/MoreThanTV.cs b/src/NzbDrone.Core/Indexers/Definitions/MoreThanTV.cs deleted file mode 100644 index 9b4d10fd5..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/MoreThanTV.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Web; -using AngleSharp.Dom; -using AngleSharp.Html.Dom; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions; - -[Obsolete("Converted to Torznab")] -public class MoreThanTV : TorrentIndexerBase<CookieTorrentBaseSettings> -{ - public override string Name => "MoreThanTV"; - public override string[] IndexerUrls => new[] { "https://www.morethantv.me/" }; - public override string Description => "Private torrent tracker for TV / MOVIES"; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override IndexerCapabilities Capabilities => SetCapabilities(); - public override bool FollowRedirect => true; - - public MoreThanTV(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - => new MoreThanTVRequestGenerator(Settings, Capabilities); - - public override IParseIndexerResponse GetParser() - => new MoreThanTVParser - { - Settings = Settings - }; - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List<TvSearchParam> - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep - }, - MovieSearchParams = new List<MovieSearchParam> - { - MovieSearchParam.Q - } - }; - - caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Movies"); - caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV"); - - return caps; - } - - protected override IDictionary<string, string> GetCookies() - { - return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); - } -} - -public class MoreThanTVRequestGenerator : IIndexerRequestGenerator -{ - private CookieTorrentBaseSettings Settings { get; } - private IndexerCapabilities Capabilities { get; } - - private NameValueCollection BrowserHeaders { get; } - - public MoreThanTVRequestGenerator(CookieTorrentBaseSettings settings, IndexerCapabilities capabilities) - { - Settings = settings; - Capabilities = capabilities; - BrowserHeaders = new NameValueCollection() - { - { "referer", settings.BaseUrl }, - { "Upgrade-Insecure-Requests", "1" }, - { "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36" } - }; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - => PerformRequest(searchCriteria); - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - => PerformRequest(searchCriteria); - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - => PerformRequest(searchCriteria); - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - => PerformRequest(searchCriteria); - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - => PerformRequest(searchCriteria); - - public Func<IDictionary<string, string>> GetCookies { get; set; } - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } - - private IndexerPageableRequestChain PerformRequest(SearchCriteriaBase query) - { - var chain = new IndexerPageableRequestChain(); - - var requests = new List<IndexerRequest> { new (new HttpRequest(GetTorrentSearchUrl(query)) { Headers = new HttpHeader(BrowserHeaders), AllowAutoRedirect = true }) }; - - if (query is TvSearchCriteria tvSearchCriteria) - { - // Always search for torrent groups (complete seasons) too - var seasonRegex = new Regex(@".*\s[Ss]{1}\d{2}([Ee]{1}\d{2,3})?$", RegexOptions.Compiled); - var seasonMatch = seasonRegex.Match(query.SanitizedSearchTerm); - if (seasonMatch.Success) - { - var seasonReplaceRegex = new Regex(@"[Ss]{1}\d{2}([Ee]{1}\d{2,3})?", RegexOptions.Compiled); - var newSearchQuery = seasonReplaceRegex.Replace(query.SanitizedSearchTerm, $"Season {tvSearchCriteria.Season}"); - requests.Add(new IndexerRequest(new HttpRequest(GetTorrentSearchUrl(query, newSearchQuery)) { Headers = new HttpHeader(BrowserHeaders), AllowAutoRedirect = true })); - } - } - - chain.Add(requests); - - return chain; - } - - private string GetTorrentSearchUrl(SearchCriteriaBase query, string overrideSearchTerm = null) - { - var qc = new NameValueCollection - { - { "action", "advanced" }, - { "sizetype", "gb" }, - { "sizerange", "0.01" }, - { "title", overrideSearchTerm ?? GetSearchString(query.SanitizedSearchTerm) } - }; - - switch (query) - { - case MovieSearchCriteria: - qc.Add("filter_cat[1]", "1"); // HD Movies - qc.Add("filter_cat[2]", "1"); // SD Movies - break; - case TvSearchCriteria: - qc.Add("filter_cat[3]", "1"); // HD Episode - qc.Add("filter_cat[4]", "1"); // SD Episode - qc.Add("filter_cat[5]", "1"); // HD Season - qc.Add("filter_cat[6]", "1"); // SD Season - break; - } - - return $"{Settings.BaseUrl}torrents/browse?{qc.GetQueryString()}"; - } - - private string GetSearchString(string input) - { - input = input.Replace("Marvels", "Marvel"); // strip 's for better results - var regex = new Regex(@"(S\d{2})$", RegexOptions.Compiled); - return regex.Replace(input, "$1*"); // If we're just seaching for a season (no episode) append an * to include all episodes of that season. - } -} - -public class MoreThanTVParser : IParseIndexerResponse -{ - public CookieTorrentBaseSettings Settings { get; init; } - - public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) - { - var releases = new List<ReleaseInfo>(); - - try - { - var parser = new HtmlParser(); - using var document = parser.ParseDocument(indexerResponse.Content); - var torrents = document.QuerySelectorAll("#torrent_table > tbody > tr.torrent"); - var movies = new[] { "movie" }; - var tv = new[] { "season", "episode" }; - - // Loop through all torrents checking for groups - foreach (var torrent in torrents) - { - // Parse required data - var downloadAnchor = torrent.QuerySelector("span a[href^=\"/torrents.php?action=download\"]"); - - if (downloadAnchor == null) - { - continue; - } - - var title = downloadAnchor.ParentElement.ParentElement.ParentElement.QuerySelector("a[class=\"overlay_torrent\"]").TextContent.Trim(); - title = CleanUpTitle(title); - - var category = torrent.QuerySelector(".cats_col div").GetAttribute("title"); - - // default to Other - var indexerCategory = NewznabStandardCategory.Other; - - if (movies.Any(category.Contains)) - { - indexerCategory = NewznabStandardCategory.Movies; - } - else if (tv.Any(category.Contains)) - { - indexerCategory = NewznabStandardCategory.TV; - } - - releases.Add(GetReleaseInfo(torrent, downloadAnchor, title, indexerCategory)); - } - - return releases; - } - catch (Exception ex) - { - throw new Exception("Error while parsing torrent response", ex); - } - } - - /// <summary> - /// Gather Release info from torrent table. Target using css - /// </summary> - /// <param name="row"></param> - /// <param name="downloadAnchor"></param> - /// <param name="title"></param> - /// <param name="category"></param> - /// <returns></returns> - private ReleaseInfo GetReleaseInfo(IElement row, IElement downloadAnchor, string title, IndexerCategory category) - { - // count from bottom - const int FILES_COL = 7; - /*const int COMMENTS_COL = 7;*/ - const int DATE_COL = 6; - const int FILESIZE_COL = 5; - const int SNATCHED_COL = 4; - const int SEEDS_COL = 3; - const int LEECHERS_COL = 2; - /*const int USER_COL = 1;*/ - - var downloadAnchorHref = (downloadAnchor as IHtmlAnchorElement).Href; - var queryParams = HttpUtility.ParseQueryString(downloadAnchorHref, Encoding.UTF8); - var torrentId = queryParams["id"]; - - var qFiles = row.QuerySelector("td:nth-last-child(" + FILES_COL + ")").TextContent; - - var fileCount = ParseUtil.CoerceInt(qFiles); - var qPublishDate = row.QuerySelector("td:nth-last-child(" + DATE_COL + ") .time").Attributes["title"].Value; - var publishDate = DateTime.ParseExact(qPublishDate, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); - var qPoster = row.QuerySelector("div.tp-banner img")?.GetAttribute("src"); - var poster = (qPoster != null && !qPoster.Contains("caticons")) ? qPoster : null; - var description = row.QuerySelector("div.tags")?.TextContent.Trim(); - var fileSize = row.QuerySelector("td:nth-last-child(" + FILESIZE_COL + ")").TextContent.Trim(); - var snatched = row.QuerySelector("td:nth-last-child(" + SNATCHED_COL + ")").TextContent.Trim(); - var seeds = row.QuerySelector("td:nth-last-child(" + SEEDS_COL + ")").TextContent.Trim(); - var leechs = row.QuerySelector("td:nth-last-child(" + LEECHERS_COL + ")").TextContent.Trim(); - - if (fileSize.Length <= 0 || snatched.Length <= 0 || seeds.Length <= 0 || leechs.Length <= 0) - { - // Size (xx.xx GB[ (Max)]) Snatches (xx) Seeders (xx) Leechers (xx) - throw new Exception($"We expected 4 torrent datas."); - } - - var detailUrl = $"{Settings.BaseUrl}details.php"; - - var size = ParseUtil.GetBytes(fileSize); - var grabs = int.Parse(snatched, NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - var seeders = int.Parse(seeds, NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - var leechers = int.Parse(leechs, NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - var detailsUrl = $"{detailUrl}?torrentid={torrentId}"; - var downloadUrl = $"{detailUrl}?action=download&id={torrentId}"; - var categories = new List<IndexerCategory> { category }; - - return new TorrentInfo - { - Title = title, - Categories = categories, - DownloadUrl = downloadUrl, - PublishDate = publishDate, - PosterUrl = poster, - Description = description, - Seeders = seeders, - Peers = seeders + leechers, - Files = fileCount, - Size = size, - Grabs = grabs, - Guid = downloadUrl, - InfoUrl = detailsUrl, - DownloadVolumeFactor = 0, // ratioless tracker - UploadVolumeFactor = 1 - }; - } - - /// <summary> - /// Clean Up any title stuff - /// </summary> - /// <param name="title"></param> - /// <returns></returns> - private string CleanUpTitle(string title) - { - return title - .Replace(".", " ") - .Replace("4K", "2160p"); // sonarr cleanup - } - - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs b/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs index b9bb49601..dad8fe8e8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/MyAnonamouse.cs @@ -7,6 +7,7 @@ using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation; +using FluentValidation.Results; using Newtonsoft.Json; using NLog; using NzbDrone.Common.Cache; @@ -15,12 +16,14 @@ using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Definitions @@ -34,8 +37,8 @@ namespace NzbDrone.Core.Indexers.Definitions public override bool SupportsPagination => true; public override int PageSize => 100; public override IndexerCapabilities Capabilities => SetCapabilities(); + private readonly ICacheManager _cacheManager; - private static readonly Regex TorrentIdRegex = new Regex(@"tor/download.php\?tid=(?<id>\d+)$"); public MyAnonamouse(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, ICacheManager cacheManager) : base(httpClient, eventAggregator, indexerStatusService, configService, logger) @@ -45,58 +48,103 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new MyAnonamouseRequestGenerator { Settings = Settings, Capabilities = Capabilities }; + return new MyAnonamouseRequestGenerator(Settings, Capabilities, _logger); } public override IParseIndexerResponse GetParser() { - return new MyAnonamouseParser(Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger); + return new MyAnonamouseParser(Definition, Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger); } - public override async Task<byte[]> Download(Uri link) + public override async Task<IndexerDownloadResponse> Download(Uri link) { - if (Settings.Freeleech) - { - _logger.Debug($"Attempting to use freeleech token for {link.AbsoluteUri}"); + var downloadLink = link.RemoveQueryParam("canUseToken"); - var idMatch = TorrentIdRegex.Match(link.AbsoluteUri); - if (idMatch.Success) + if (Settings.UseFreeleechWedge is (int)MyAnonamouseFreeleechWedgeAction.Preferred or (int)MyAnonamouseFreeleechWedgeAction.Required && + bool.TryParse(link.GetQueryParam("canUseToken"), out var canUseToken) && canUseToken) + { + _logger.Debug("Attempting to use freeleech wedge for {0}", downloadLink.AbsoluteUri); + + if (int.TryParse(link.GetQueryParam("tid"), out var torrentId) && torrentId > 0) { - var id = int.Parse(idMatch.Groups["id"].Value); var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); var freeleechUrl = Settings.BaseUrl + $"json/bonusBuy.php/{timestamp}"; - var freeleechRequest = new HttpRequestBuilder(freeleechUrl) + var freeleechRequestBuilder = new HttpRequestBuilder(freeleechUrl) + .Accept(HttpAccept.Json) .AddQueryParam("spendtype", "personalFL") - .AddQueryParam("torrentid", id) - .AddQueryParam("timestamp", timestamp.ToString()) - .Build(); + .AddQueryParam("torrentid", torrentId) + .AddQueryParam("timestamp", timestamp.ToString()); - var indexerReq = new IndexerRequest(freeleechRequest); - var response = await FetchIndexerResponse(indexerReq).ConfigureAwait(false); - var resource = Json.Deserialize<MyAnonamouseBuyPersonalFreeleechResponse>(response.Content); + freeleechRequestBuilder.LogResponseContent = true; + + var cookies = GetCookies(); + + if (cookies != null && cookies.Any()) + { + freeleechRequestBuilder.SetCookies(cookies); + } + + var freeleechRequest = freeleechRequestBuilder.Build(); + + var freeleechResponse = await _httpClient.ExecuteProxiedAsync(freeleechRequest, Definition).ConfigureAwait(false); + + var resource = Json.Deserialize<MyAnonamouseBuyPersonalFreeleechResponse>(freeleechResponse.Content); if (resource.Success) { - _logger.Debug($"Successfully to used freeleech token for torrentid ${id}"); + _logger.Debug("Successfully used freeleech wedge for torrentid {0}.", torrentId); + } + else if (resource.Error.IsNotNullOrWhiteSpace() && resource.Error.ContainsIgnoreCase("This Torrent is VIP")) + { + _logger.Debug("{0} is already VIP, continuing downloading: {1}", torrentId, resource.Error); + } + else if (resource.Error.IsNotNullOrWhiteSpace() && resource.Error.ContainsIgnoreCase("This is already a personal freeleech")) + { + _logger.Debug("{0} is already a personal freeleech, continuing downloading: {1}", torrentId, resource.Error); } else { - _logger.Debug($"Failed to use freeleech token: ${resource.Error}"); + _logger.Warn("Failed to purchase freeleech wedge for {0}: {1}", torrentId, resource.Error); + + if (Settings.UseFreeleechWedge == (int)MyAnonamouseFreeleechWedgeAction.Preferred) + { + _logger.Debug("'Use Freeleech Wedge' option set to preferred, continuing downloading: '{0}'", downloadLink.AbsoluteUri); + } + else + { + throw new ReleaseUnavailableException($"Failed to buy freeleech wedge and 'Use Freeleech Wedge' is set to required, aborting download: '{downloadLink.AbsoluteUri}'"); + } } } else { - _logger.Debug($"Could not get torrent id from link ${link.AbsoluteUri}, skipping freeleech"); + _logger.Warn("Could not get torrent id from link {0}, skipping use of freeleech wedge.", downloadLink.AbsoluteUri); } } - return await base.Download(link).ConfigureAwait(false); + return await base.Download(downloadLink).ConfigureAwait(false); } protected override IDictionary<string, string> GetCookies() { - return CookieUtil.CookieHeaderToDictionary("mam_id=" + Settings.MamId); + var cookies = base.GetCookies(); + + if (cookies is { Count: > 0 } && cookies.TryGetValue("mam_id", out var mamId) && mamId.IsNotNullOrWhiteSpace()) + { + return cookies; + } + + return CookieUtil.CookieHeaderToDictionary($"mam_id={Settings.MamId}"); + } + + protected override async Task Test(List<ValidationFailure> failures) + { + UpdateCookies(null, null); + + _logger.Debug("Cookies cleared."); + + await base.Test(failures).ConfigureAwait(false); } private IndexerCapabilities SetCapabilities() @@ -210,14 +258,31 @@ namespace NzbDrone.Core.Indexers.Definitions public class MyAnonamouseRequestGenerator : IIndexerRequestGenerator { - public MyAnonamouseSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } + private static readonly Regex SanitizeSearchQueryRegex = new ("[^\\w]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private readonly MyAnonamouseSettings _settings; + private readonly IndexerCapabilities _capabilities; + private readonly Logger _logger; + + public MyAnonamouseRequestGenerator(MyAnonamouseSettings settings, IndexerCapabilities capabilities, Logger logger) + { + _settings = settings; + _capabilities = capabilities; + _logger = logger; + } private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria) { - var term = searchCriteria.SanitizedSearchTerm.Trim(); + var term = SanitizeSearchQueryRegex.Replace(searchCriteria.SanitizedSearchTerm, " ").Trim(); - var searchType = Settings.SearchType switch + if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && term.IsNullOrWhiteSpace()) + { + _logger.Debug("Search term is empty after being sanitized, stopping search. Initial search term: '{0}'", searchCriteria.SearchTerm); + + yield break; + } + + var searchType = _settings.SearchType switch { (int)MyAnonamouseSearchType.Active => "active", (int)MyAnonamouseSearchType.Freeleech => "fl", @@ -242,37 +307,59 @@ namespace NzbDrone.Core.Indexers.Definitions { "description", "1" } // include the description }; - if (Settings.SearchInDescription) + if (_settings.SearchInDescription) { - parameters.Add("tor[srchIn][description]", "true"); + parameters.Set("tor[srchIn][description]", "true"); } - if (Settings.SearchInSeries) + if (_settings.SearchInSeries) { - parameters.Add("tor[srchIn][series]", "true"); + parameters.Set("tor[srchIn][series]", "true"); } - if (Settings.SearchInFilenames) + if (_settings.SearchInFilenames) { - parameters.Add("tor[srchIn][filenames]", "true"); + parameters.Set("tor[srchIn][filenames]", "true"); } - var catList = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories); + if (_settings.SearchLanguages.Any()) + { + foreach (var (language, index) in _settings.SearchLanguages.Select((value, index) => (value, index))) + { + parameters.Set($"tor[browse_lang][{index}]", language.ToString()); + } + } + + var catList = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList(); + if (catList.Any()) { - var index = 0; - foreach (var cat in catList) + foreach (var (category, index) in catList.Select((value, index) => (value, index))) { - parameters.Add("tor[cat][" + index + "]", cat); - index++; + parameters.Set($"tor[cat][{index}]", category); } } else { - parameters.Add("tor[cat][]", "0"); + parameters.Set("tor[cat][]", "0"); } - var searchUrl = Settings.BaseUrl + "tor/js/loadSearchJSONbasic.php"; + if (searchCriteria.MinSize is > 0) + { + parameters.Set("tor[minSize]", searchCriteria.MinSize.Value.ToString()); + } + + if (searchCriteria.MaxSize is > 0) + { + parameters.Set("tor[maxSize]", searchCriteria.MaxSize.Value.ToString()); + } + + if (searchCriteria.MinSize is > 0 || searchCriteria.MaxSize is > 0) + { + parameters.Set("tor[unit]", "1"); + } + + var searchUrl = _settings.BaseUrl + "tor/js/loadSearchJSONbasic.php"; if (parameters.Count > 0) { @@ -323,66 +410,78 @@ namespace NzbDrone.Core.Indexers.Definitions public class MyAnonamouseParser : IParseIndexerResponse { + private readonly ProviderDefinition _definition; private readonly MyAnonamouseSettings _settings; private readonly IndexerCapabilitiesCategories _categories; private readonly IIndexerHttpClient _httpClient; private readonly Logger _logger; + private readonly ICached<string> _userClassCache; private readonly HashSet<string> _vipFreeleechUserClasses = new (StringComparer.OrdinalIgnoreCase) { "VIP", - "Elite VIP", + "Elite VIP" }; - public MyAnonamouseParser(MyAnonamouseSettings settings, + public MyAnonamouseParser(ProviderDefinition definition, + MyAnonamouseSettings settings, IndexerCapabilitiesCategories categories, IIndexerHttpClient httpClient, ICacheManager cacheManager, Logger logger) { + _definition = definition; _settings = settings; _categories = categories; _httpClient = httpClient; _logger = logger; + _userClassCache = cacheManager.GetCache<string>(GetType()); } public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) { + var httpResponse = indexerResponse.HttpResponse; + // Throw auth errors here before we try to parse - if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Forbidden) + if (httpResponse.StatusCode == HttpStatusCode.Forbidden) { throw new IndexerAuthException("[403 Forbidden] - mam_id expired or invalid"); } // Throw common http errors here before we try to parse - if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + if (httpResponse.StatusCode != HttpStatusCode.OK) { // Remove cookie cache CookiesUpdater(null, null); - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + throw new IndexerException(indexerResponse, $"Unexpected response status {httpResponse.StatusCode} code from indexer request"); } - if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + if (!httpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) { // Remove cookie cache CookiesUpdater(null, null); - throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); + throw new IndexerException(indexerResponse, $"Unexpected response header {httpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}"); } - var torrentInfos = new List<TorrentInfo>(); + var releaseInfos = new List<ReleaseInfo>(); var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseResponse>(indexerResponse.Content); var error = jsonResponse.Error; - if (error is "Nothing returned, out of 0" or "Nothing returned, out of 1") + if (error.IsNotNullOrWhiteSpace() && error.StartsWithIgnoreCase("Nothing returned, out of")) { - return torrentInfos.ToArray(); + return releaseInfos.ToArray(); } - var hasUserVip = HasUserVip(); + if (jsonResponse.Data == null) + { + throw new IndexerException(indexerResponse, "Unexpected response content from indexer request: {0}", jsonResponse.Message ?? "Check the logs for more information."); + } + + var hasUserVip = HasUserVip(httpResponse.GetCookies()); foreach (var item in jsonResponse.Data) { @@ -394,6 +493,8 @@ namespace NzbDrone.Core.Indexers.Definitions release.Title = item.Title; release.Description = item.Description; + release.BookTitle = item.Title; + if (item.AuthorInfo != null) { var authorInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.AuthorInfo); @@ -402,6 +503,7 @@ namespace NzbDrone.Core.Indexers.Definitions if (author.IsNotNullOrWhiteSpace()) { release.Title += " by " + author; + release.Author = author; } } @@ -429,8 +531,10 @@ namespace NzbDrone.Core.Indexers.Definitions release.Title += " [VIP]"; } - release.DownloadUrl = _settings.BaseUrl + "tor/download.php?tid=" + id; - release.InfoUrl = _settings.BaseUrl + "t/" + id; + var isFreeLeech = item.Free || item.PersonalFreeLeech || (hasUserVip && item.FreeVip); + + release.DownloadUrl = GetDownloadUrl(id, !isFreeLeech); + release.InfoUrl = $"{_settings.BaseUrl}t/{id}"; release.Guid = release.InfoUrl; release.Categories = _categories.MapTrackerCatToNewznab(item.Category); release.PublishDate = DateTime.ParseExact(item.Added, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); @@ -439,18 +543,35 @@ namespace NzbDrone.Core.Indexers.Definitions release.Seeders = item.Seeders; release.Peers = item.Leechers + release.Seeders; release.Size = ParseUtil.GetBytes(item.Size); - release.DownloadVolumeFactor = item.Free ? 0 : hasUserVip && item.FreeVip ? 0 : 1; + release.DownloadVolumeFactor = isFreeLeech ? 0 : 1; release.UploadVolumeFactor = 1; release.MinimumRatio = 1; release.MinimumSeedTime = 259200; // 72 hours - torrentInfos.Add(release); + releaseInfos.Add(release); } - return torrentInfos.ToArray(); + // Update cookies with the updated mam_id value received in the response + CookiesUpdater(httpResponse.GetCookies(), DateTime.Now.AddDays(30)); + + return releaseInfos.ToArray(); } - private bool HasUserVip() + private string GetDownloadUrl(int torrentId, bool canUseToken) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/tor/download.php") + .AddQueryParam("tid", torrentId); + + if (_settings.UseFreeleechWedge is (int)MyAnonamouseFreeleechWedgeAction.Preferred or (int)MyAnonamouseFreeleechWedgeAction.Required && canUseToken) + { + url = url.AddQueryParam("canUseToken", "true"); + } + + return url.FullUri; + } + + private bool HasUserVip(Dictionary<string, string> cookies) { var cacheKey = "myanonamouse_user_class_" + _settings.ToJson().SHA256Hash(); @@ -461,15 +582,17 @@ namespace NzbDrone.Core.Indexers.Definitions var request = new HttpRequestBuilder(_settings.BaseUrl.Trim('/')) .Resource("/jsonLoad.php") .Accept(HttpAccept.Json) + .SetCookies(cookies) .Build(); - _logger.Debug("Fetching user data: " + request.Url.FullUri); + _logger.Debug("Fetching user data: {0}", request.Url.FullUri); - request.Cookies.Add("mam_id", _settings.MamId); + var response = _httpClient.ExecuteProxied(request, _definition); - var response = _httpClient.Get(request); var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseUserDataResponse>(response.Content); + _logger.Trace("Current user class: '{0}'", jsonResponse.UserClass); + return jsonResponse.UserClass?.Trim(); }, TimeSpan.FromHours(1)); @@ -499,6 +622,8 @@ namespace NzbDrone.Core.Indexers.Definitions SearchInDescription = false; SearchInSeries = false; SearchInFilenames = false; + SearchLanguages = Array.Empty<int>(); + UseFreeleechWedge = (int)MyAnonamouseFreeleechWedgeAction.Never; } [FieldDefinition(2, Type = FieldType.Textbox, Label = "Mam Id", HelpText = "Mam Session Id (Created Under Preferences -> Security)")] @@ -507,18 +632,21 @@ namespace NzbDrone.Core.Indexers.Definitions [FieldDefinition(3, Type = FieldType.Select, Label = "Search Type", SelectOptions = typeof(MyAnonamouseSearchType), HelpText = "Specify the desired search type")] public int SearchType { get; set; } - [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Buy Freeleech Token", HelpText = "Buy personal freeleech token for download")] - public bool Freeleech { get; set; } - - [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Search in description", HelpText = "Search text in the description")] + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Search in description", HelpText = "Search text in the description")] public bool SearchInDescription { get; set; } - [FieldDefinition(6, Type = FieldType.Checkbox, Label = "Search in series", HelpText = "Search text in the series")] + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Search in series", HelpText = "Search text in the series")] public bool SearchInSeries { get; set; } - [FieldDefinition(7, Type = FieldType.Checkbox, Label = "Search in filenames", HelpText = "Search text in the filenames")] + [FieldDefinition(6, Type = FieldType.Checkbox, Label = "Search in filenames", HelpText = "Search text in the filenames")] public bool SearchInFilenames { get; set; } + [FieldDefinition(7, Type = FieldType.Select, Label = "Search Languages", SelectOptions = typeof(MyAnonamouseSearchLanguages), HelpText = "Specify the desired languages. If unspecified, all options are used.")] + public IEnumerable<int> SearchLanguages { get; set; } + + [FieldDefinition(8, Type = FieldType.Select, Label = "Use Freeleech Wedges", SelectOptions = typeof(MyAnonamouseFreeleechWedgeAction), HelpText = "Use freeleech wedges to make grabbed torrents personal freeleech")] + public int UseFreeleechWedge { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); @@ -529,18 +657,227 @@ namespace NzbDrone.Core.Indexers.Definitions { [FieldOption(Label="All torrents", Hint = "Search everything")] All = 0, + [FieldOption(Label="Only active", Hint = "Last update had 1+ seeders")] Active = 1, + [FieldOption(Label="Freeleech", Hint = "Freeleech torrents")] Freeleech = 2, + [FieldOption(Label="Freeleech or VIP", Hint = "Freeleech or VIP torrents")] FreeleechOrVip = 3, + [FieldOption(Label="VIP", Hint = "VIP torrents")] Vip = 4, + [FieldOption(Label="Not VIP", Hint = "Torrents not VIP")] NotVip = 5, } + public enum MyAnonamouseSearchLanguages + { + [FieldOption(Label="English")] + English = 1, + + [FieldOption(Label="Afrikaans")] + Afrikaans = 17, + + [FieldOption(Label="Arabic")] + Arabic = 32, + + [FieldOption(Label="Bengali")] + Bengali = 35, + + [FieldOption(Label="Bosnian")] + Bosnian = 51, + + [FieldOption(Label="Bulgarian")] + Bulgarian = 18, + + [FieldOption(Label="Burmese")] + Burmese = 6, + + [FieldOption(Label="Cantonese")] + Cantonese = 44, + + [FieldOption(Label="Catalan")] + Catalan = 19, + + [FieldOption(Label="Chinese")] + Chinese = 2, + + [FieldOption(Label="Croatian")] + Croatian = 49, + + [FieldOption(Label="Czech")] + Czech = 20, + + [FieldOption(Label="Danish")] + Danish = 21, + + [FieldOption(Label="Dutch")] + Dutch = 22, + + [FieldOption(Label="Estonian")] + Estonian = 61, + + [FieldOption(Label="Farsi")] + Farsi = 39, + + [FieldOption(Label="Finnish")] + Finnish = 23, + + [FieldOption(Label="French")] + French = 36, + + [FieldOption(Label="German")] + German = 37, + + [FieldOption(Label="Greek")] + Greek = 26, + + [FieldOption(Label="Greek, Ancient")] + GreekAncient = 59, + + [FieldOption(Label="Gujarati")] + Gujarati = 3, + + [FieldOption(Label="Hebrew")] + Hebrew = 27, + + [FieldOption(Label="Hindi")] + Hindi = 8, + + [FieldOption(Label="Hungarian")] + Hungarian = 28, + + [FieldOption(Label="Icelandic")] + Icelandic = 63, + + [FieldOption(Label="Indonesian")] + Indonesian = 53, + + [FieldOption(Label="Irish")] + Irish = 56, + + [FieldOption(Label="Italian")] + Italian = 43, + + [FieldOption(Label="Japanese")] + Japanese = 38, + + [FieldOption(Label="Javanese")] + Javanese = 12, + + [FieldOption(Label="Kannada")] + Kannada = 5, + + [FieldOption(Label="Korean")] + Korean = 41, + + [FieldOption(Label="Lithuanian")] + Lithuanian = 50, + + [FieldOption(Label="Latin")] + Latin = 46, + + [FieldOption(Label="Latvian")] + Latvian = 62, + + [FieldOption(Label="Malay")] + Malay = 33, + + [FieldOption(Label="Malayalam")] + Malayalam = 58, + + [FieldOption(Label="Manx")] + Manx = 57, + + [FieldOption(Label="Marathi")] + Marathi = 9, + + [FieldOption(Label="Norwegian")] + Norwegian = 48, + + [FieldOption(Label="Polish")] + Polish = 45, + + [FieldOption(Label="Portuguese")] + Portuguese = 34, + + [FieldOption(Label="Brazilian Portuguese (BP)")] + BrazilianPortuguese = 52, + + [FieldOption(Label="Punjabi")] + Punjabi = 14, + + [FieldOption(Label="Romanian")] + Romanian = 30, + + [FieldOption(Label="Russian")] + Russian = 16, + + [FieldOption(Label="Scottish Gaelic")] + ScottishGaelic = 24, + + [FieldOption(Label="Sanskrit")] + Sanskrit = 60, + + [FieldOption(Label="Serbian")] + Serbian = 31, + + [FieldOption(Label="Slovenian")] + Slovenian = 54, + + [FieldOption(Label="Spanish")] + Spanish = 4, + + [FieldOption(Label="Castilian Spanish")] + CastilianSpanish = 55, + + [FieldOption(Label="Swedish")] + Swedish = 40, + + [FieldOption(Label="Tagalog")] + Tagalog = 29, + + [FieldOption(Label="Tamil")] + Tamil = 11, + + [FieldOption(Label="Telugu")] + Telugu = 10, + + [FieldOption(Label="Thai")] + Thai = 7, + + [FieldOption(Label="Turkish")] + Turkish = 42, + + [FieldOption(Label="Ukrainian")] + Ukrainian = 25, + + [FieldOption(Label="Urdu")] + Urdu = 15, + + [FieldOption(Label="Vietnamese")] + Vietnamese = 13, + + [FieldOption(Label="Other")] + Other = 47, + } + + public enum MyAnonamouseFreeleechWedgeAction + { + [FieldOption(Label = "Never", Hint = "Do not buy as freeleech")] + Never = 0, + + [FieldOption(Label = "Preferred", Hint = "Buy and use wedge if possible")] + Preferred = 1, + + [FieldOption(Label = "Required", Hint = "Abort download if unable to buy wedge")] + Required = 2, + } + public class MyAnonamouseTorrent { public int Id { get; set; } @@ -553,6 +890,8 @@ namespace NzbDrone.Core.Indexers.Definitions public string Filetype { get; set; } public bool Vip { get; set; } public bool Free { get; set; } + [JsonProperty(PropertyName = "personal_freeleech")] + public bool PersonalFreeLeech { get; set; } [JsonProperty(PropertyName = "fl_vip")] public bool FreeVip { get; set; } public string Category { get; set; } @@ -568,7 +907,8 @@ namespace NzbDrone.Core.Indexers.Definitions public class MyAnonamouseResponse { public string Error { get; set; } - public List<MyAnonamouseTorrent> Data { get; set; } + public IReadOnlyCollection<MyAnonamouseTorrent> Data { get; set; } + public string Message { get; set; } } public class MyAnonamouseBuyPersonalFreeleechResponse @@ -579,7 +919,7 @@ namespace NzbDrone.Core.Indexers.Definitions public class MyAnonamouseUserDataResponse { - [JsonProperty(PropertyName = "class")] + [JsonProperty(PropertyName = "classname")] public string UserClass { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs b/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs index 45810a1c2..dcc26338e 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Text; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; using NLog; @@ -42,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new NebulanceRequestGenerator(Settings); + return new NebulanceRequestGenerator(Settings, _logger); } public override IParseIndexerResponse GetParser() @@ -68,26 +67,6 @@ namespace NzbDrone.Core.Indexers.Definitions return Task.FromResult(request); } - protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria) - { - var cleanReleases = base.CleanupReleases(releases, searchCriteria); - - return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); - } - - protected override IEnumerable<ReleaseInfo> FilterReleasesByQuery(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria) - { - if (!searchCriteria.IsRssSearch && - searchCriteria.IsIdSearch && - searchCriteria is TvSearchCriteria tvSearchCriteria && - tvSearchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace()) - { - releases = releases.Where(r => r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(tvSearchCriteria.EpisodeSearchString)).ToList(); - } - - return releases; - } - private IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities @@ -111,10 +90,12 @@ namespace NzbDrone.Core.Indexers.Definitions public class NebulanceRequestGenerator : IIndexerRequestGenerator { private readonly NebulanceSettings _settings; + private readonly Logger _logger; - public NebulanceRequestGenerator(NebulanceSettings settings) + public NebulanceRequestGenerator(NebulanceSettings settings, Logger logger) { _settings = settings; + _logger = logger; } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) @@ -136,40 +117,66 @@ namespace NzbDrone.Core.Indexers.Definitions Age = ">0" }; - if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace()) + if (searchCriteria.TvMazeId is > 0) { - queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedTvSearchString, "[\\W]+", "%").Trim() + "%"; + queryParams.TvMaze = searchCriteria.TvMazeId.Value; + } + else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace()) + { + queryParams.Imdb = searchCriteria.FullImdbId; } - if (searchCriteria.TvMazeId.HasValue) - { - queryParams.Tvmaze = searchCriteria.TvMazeId.Value; + var searchQuery = searchCriteria.SanitizedSearchTerm.Trim(); - if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace()) + if (searchQuery.IsNotNullOrWhiteSpace()) + { + queryParams.Release = searchQuery; + } + + if (searchCriteria.Season.HasValue && + searchCriteria.Episode.IsNotNullOrWhiteSpace() && + DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) + { + if (searchQuery.IsNotNullOrWhiteSpace()) { - queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%"; + queryParams.Name = searchQuery; + } + + queryParams.Release = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture); + } + else + { + if (searchCriteria.Season.HasValue) + { + queryParams.Season = searchCriteria.Season.Value; + } + + if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber)) + { + queryParams.Episode = episodeNumber; } } - else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb)) - { - queryParams.Imdb = intImdb; - if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace()) - { - queryParams.Name = "%" + Regex.Replace(searchCriteria.EpisodeSearchString, "[\\W]+", "%").Trim() + "%"; - } + if ((queryParams.Season.HasValue || queryParams.Episode.HasValue) && + queryParams.Name.IsNullOrWhiteSpace() && + queryParams.Release.IsNullOrWhiteSpace() && + !queryParams.TvMaze.HasValue && + queryParams.Imdb.IsNullOrWhiteSpace()) + { + _logger.Debug("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys."); + + return new IndexerPageableRequestChain(); + } + + if (queryParams.Name is { Length: > 0 and < 3 } || queryParams.Release is { Length: > 0 and < 3 }) + { + _logger.Debug("NBL API does not support release calls that are 2 characters or fewer."); + + return new IndexerPageableRequestChain(); } pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset)); - if (queryParams.Name.IsNotNullOrWhiteSpace() && (queryParams.Tvmaze is > 0 || queryParams.Imdb is > 0)) - { - queryParams = queryParams.Clone(); - queryParams.Name = null; - - pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset)); - } - return pageableRequests; } @@ -187,9 +194,18 @@ namespace NzbDrone.Core.Indexers.Definitions Age = ">0" }; - if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace()) + var searchQuery = searchCriteria.SanitizedSearchTerm.Trim(); + + if (searchQuery.IsNotNullOrWhiteSpace()) { - queryParams.Name = "%" + Regex.Replace(searchCriteria.SanitizedSearchTerm, "[\\W]+", "%").Trim() + "%"; + queryParams.Release = searchQuery; + } + + if (queryParams.Release is { Length: > 0 and < 3 }) + { + _logger.Debug("NBL API does not support release calls that are 2 characters or fewer."); + + return new IndexerPageableRequestChain(); } pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset)); @@ -228,17 +244,30 @@ namespace NzbDrone.Core.Indexers.Definitions if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize<JsonRpcResponse<NebulanceErrorResponse>>(indexerResponse.HttpResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request: {1}", indexerResponse.HttpResponse.StatusCode, errorResponse?.Result?.Error?.Message ?? "Check the logs for more information."); } - var jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse.Content); + JsonRpcResponse<NebulanceResponse> jsonResponse; + + try + { + jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceResponse>>(indexerResponse.HttpResponse.Content); + } + catch (Exception ex) + { + STJson.TryDeserialize<JsonRpcResponse<string>>(indexerResponse.HttpResponse.Content, out var response); + + throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", ex, response?.Result ?? ex.Message); + } if (jsonResponse.Error != null || jsonResponse.Result == null) { throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error); } - if (jsonResponse.Result.Items.Count == 0) + if (jsonResponse.Result?.Items == null || jsonResponse.Result.Items.Count == 0) { return torrentInfos; } @@ -253,14 +282,13 @@ namespace NzbDrone.Core.Indexers.Definitions var release = new TorrentInfo { - Title = title, Guid = details, InfoUrl = details, - PosterUrl = row.Banner, DownloadUrl = row.Download, + Title = title.Trim(), Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) }, Size = ParseUtil.CoerceLong(row.Size), - Files = row.FileList.Length, + Files = row.FileList.Count(), PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), Grabs = ParseUtil.CoerceInt(row.Snatch), Seeders = ParseUtil.CoerceInt(row.Seed), @@ -269,7 +297,8 @@ namespace NzbDrone.Core.Indexers.Definitions MinimumRatio = 0, // ratioless MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes DownloadVolumeFactor = 0, // ratioless tracker - UploadVolumeFactor = 1 + UploadVolumeFactor = 1, + PosterUrl = row.Banner }; if (row.TvMazeId.IsNotNullOrWhiteSpace()) @@ -293,7 +322,7 @@ namespace NzbDrone.Core.Indexers.Definitions ApiKey = ""; } - [FieldDefinition(4, Label = "API Key", HelpText = "API Key from User Settings > Api Keys. Key must have List and Download permissions")] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerNebulanceSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } } @@ -301,60 +330,96 @@ namespace NzbDrone.Core.Indexers.Definitions { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Id { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Time { get; set; } + [JsonProperty(PropertyName="age", DefaultValueHandling = DefaultValueHandling.Ignore)] public string Age { get; set; } + [JsonProperty(PropertyName="tvmaze", DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? Tvmaze { get; set; } + public int? TvMaze { get; set; } + [JsonProperty(PropertyName="imdb", DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? Imdb { get; set; } + public string Imdb { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Hash { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string[] Tags { get; set; } + [JsonProperty(PropertyName="name", DefaultValueHandling = DefaultValueHandling.Ignore)] public string Name { get; set; } + + [JsonProperty(PropertyName="release", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Release { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Category { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string Series { get; set; } + [JsonProperty(PropertyName="season", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Season { get; set; } + + [JsonProperty(PropertyName="episode", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Episode { get; set; } + public NebulanceQuery Clone() { return MemberwiseClone() as NebulanceQuery; } } + public class NebulanceResponse + { + public List<NebulanceTorrent> Items { get; set; } + } + public class NebulanceTorrent { [JsonPropertyName("rls_name")] public string ReleaseTitle { get; set; } + [JsonPropertyName("cat")] public string Category { get; set; } + public string Size { get; set; } public string Seed { get; set; } public string Leech { get; set; } public string Snatch { get; set; } public string Download { get; set; } + [JsonPropertyName("file_list")] - public string[] FileList { get; set; } + public IEnumerable<string> FileList { get; set; } = Array.Empty<string>(); + [JsonPropertyName("group_name")] public string GroupName { get; set; } + [JsonPropertyName("series_banner")] public string Banner { get; set; } + [JsonPropertyName("group_id")] public string TorrentId { get; set; } + [JsonPropertyName("series_id")] public string TvMazeId { get; set; } + [JsonPropertyName("rls_utc")] public string PublishDateUtc { get; set; } - public IEnumerable<string> Tags { get; set; } + + public IEnumerable<string> Tags { get; set; } = Array.Empty<string>(); } - public class NebulanceTorrents + public class NebulanceErrorResponse { - public List<NebulanceTorrent> Items { get; set; } - public int Results { get; set; } + public NebulanceErrorMessage Error { get; set; } + } + + public class NebulanceErrorMessage + { + public string Message { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs index 882cb6e5e..8230a71d8 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/Newznab.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Newznab public string[] GetBaseUrlFromSettings() { - if (Definition == null || Settings?.Categories == null) + if (Definition == null || Settings?.Capabilities == null) { return new[] { "" }; } @@ -61,16 +61,23 @@ namespace NzbDrone.Core.Indexers.Newznab { var caps = new IndexerCapabilities(); - if (Definition == null || Settings?.Categories == null) + if (Definition == null || Settings?.Capabilities?.Categories == null) { return caps; } - foreach (var category in Settings.Categories) + foreach (var category in Settings.Capabilities.Categories) { caps.Categories.AddCategoryMapping(category.Name, category); } + caps.SupportsRawSearch = Settings?.Capabilities?.SupportsRawSearch ?? false; + caps.SearchParams = Settings?.Capabilities?.SearchParams ?? new List<SearchParam> { SearchParam.Q }; + caps.TvSearchParams = Settings?.Capabilities?.TvSearchParams ?? new List<TvSearchParam>(); + caps.MovieSearchParams = Settings?.Capabilities?.MovieSearchParams ?? new List<MovieSearchParam>(); + caps.MusicSearchParams = Settings?.Capabilities?.MusicSearchParams ?? new List<MusicSearchParam>(); + caps.BookSearchParams = Settings?.Capabilities?.BookSearchParams ?? new List<BookSearchParam>(); + return caps; } @@ -84,25 +91,24 @@ namespace NzbDrone.Core.Indexers.Newznab { get { - yield return GetDefinition("abNZB", GetSettings("https://abnzb.com")); - yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za")); - yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org")); - yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr")); - yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com")); + yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"), categories: new[] { 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"), categories: new[] { 2000, 3000, 4000, 5000, 7000 }); + yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"), categories: new[] { 2020, 5070 }); + yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000 }); yield return GetDefinition("GingaDADDY", GetSettings("https://www.gingadaddy.com")); - yield return GetDefinition("Miatrix", GetSettings("https://www.miatrix.com")); - yield return GetDefinition("Newz-Complex", GetSettings("https://newz-complex.org/www")); - yield return GetDefinition("Newz69", GetSettings("https://newz69.keagaming.com")); - yield return GetDefinition("NinjaCentral", GetSettings("https://ninjacentral.co.za")); - yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su")); - yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat")); - yield return GetDefinition("NZBFinder", GetSettings("https://nzbfinder.ws")); - yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info")); - yield return GetDefinition("NzbNoob", GetSettings("https://www.nzbnoob.com")); - yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com")); - yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net")); - yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com")); - yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api")); + yield return GetDefinition("Miatrix", GetSettings("https://www.miatrix.com"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("Newz69", GetSettings("https://newz69.keagaming.com"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NinjaCentral", GetSettings("https://ninjacentral.co.za"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000 }); + yield return GetDefinition("NZBFinder", GetSettings("https://nzbfinder.ws"), categories: new[] { 2000, 3000, 5000, 6000, 7000 }); + yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NzbNoob", GetSettings("https://www.nzbnoob.com"), categories: new[] { 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000 }); + yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000 }); + yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"), categories: new[] { 1000, 2000, 3000, 4000, 5000, 6000, 7000 }); yield return GetDefinition("Generic Newznab", GetSettings("")); } } @@ -113,8 +119,23 @@ namespace NzbDrone.Core.Indexers.Newznab _capabilitiesProvider = capabilitiesProvider; } - private IndexerDefinition GetDefinition(string name, NewznabSettings settings) + private IndexerDefinition GetDefinition(string name, NewznabSettings settings, IEnumerable<int> categories = null) { + var caps = new IndexerCapabilities(); + + if (categories != null) + { + foreach (var categoryId in categories) + { + var mappedCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == categoryId); + + if (mappedCat != null) + { + caps.Categories.AddCategoryMapping(mappedCat.Id, mappedCat); + } + } + } + return new IndexerDefinition { Enable = true, @@ -127,7 +148,7 @@ namespace NzbDrone.Core.Indexers.Newznab SupportsSearch = SupportsSearch, SupportsRedirect = SupportsRedirect, SupportsPagination = SupportsPagination, - Capabilities = Capabilities + Capabilities = caps }; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs index e182b97e7..f72f0a9f9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesProvider.cs @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var param in xmlBasicSearch.Attribute("supportedParams").Value.Split(',')) { - if (Enum.TryParse(param, true, out SearchParam searchParam)) + if (Enum.TryParse(param, true, out SearchParam searchParam) && !capabilities.SearchParams.Contains(searchParam)) { capabilities.SearchParams.AddIfNotNull(searchParam); } @@ -146,7 +146,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var param in xmlMovieSearch.Attribute("supportedParams").Value.Split(',')) { - if (Enum.TryParse(param, true, out MovieSearchParam searchParam)) + if (Enum.TryParse(param, true, out MovieSearchParam searchParam) && !capabilities.MovieSearchParams.Contains(searchParam)) { capabilities.MovieSearchParams.AddIfNotNull(searchParam); } @@ -166,7 +166,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var param in xmlTvSearch.Attribute("supportedParams").Value.Split(',')) { - if (Enum.TryParse(param, true, out TvSearchParam searchParam)) + if (Enum.TryParse(param, true, out TvSearchParam searchParam) && !capabilities.TvSearchParams.Contains(searchParam)) { capabilities.TvSearchParams.AddIfNotNull(searchParam); } @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var param in xmlAudioSearch.Attribute("supportedParams").Value.Split(',')) { - if (Enum.TryParse(param, true, out MusicSearchParam searchParam)) + if (Enum.TryParse(param, true, out MusicSearchParam searchParam) && !capabilities.MusicSearchParams.Contains(searchParam)) { capabilities.MusicSearchParams.AddIfNotNull(searchParam); } @@ -206,7 +206,7 @@ namespace NzbDrone.Core.Indexers.Newznab { foreach (var param in xmlBookSearch.Attribute("supportedParams").Value.Split(',')) { - if (Enum.TryParse(param, true, out BookSearchParam searchParam)) + if (Enum.TryParse(param, true, out BookSearchParam searchParam) && !capabilities.BookSearchParams.Contains(searchParam)) { capabilities.BookSearchParams.AddIfNotNull(searchParam); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesSettings.cs new file mode 100644 index 000000000..ed59797af --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabCapabilitiesSettings.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.Newznab; + +public class NewznabCapabilitiesSettings +{ + public bool SupportsRawSearch { get; set; } + + public List<SearchParam> SearchParams { get; set; } + + public List<TvSearchParam> TvSearchParams { get; set; } + + public List<MovieSearchParam> MovieSearchParams { get; set; } + + public List<MusicSearchParam> MusicSearchParams { get; set; } + + public List<BookSearchParam> BookSearchParams { get; set; } + + public List<IndexerCategory> Categories { get; set; } + + public NewznabCapabilitiesSettings() + { + } + + public NewznabCapabilitiesSettings(IndexerCapabilities capabilities) + { + SupportsRawSearch = capabilities?.SupportsRawSearch ?? false; + SearchParams = capabilities?.SearchParams; + TvSearchParams = capabilities?.TvSearchParams; + MovieSearchParams = capabilities?.MovieSearchParams; + MusicSearchParams = capabilities?.MusicSearchParams; + BookSearchParams = capabilities?.BookSearchParams; + Categories = capabilities?.Categories.GetTorznabCategoryList(); + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs index f9ab56128..ad3f48b07 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRequestGenerator.cs @@ -128,7 +128,7 @@ namespace NzbDrone.Core.Indexers.Newznab parameters.Set("tvdbid", searchCriteria.TvdbId.Value.ToString()); } - if (searchCriteria.TmdbId.HasValue && capabilities.TvSearchTvdbAvailable) + if (searchCriteria.TmdbId.HasValue && capabilities.TvSearchTmdbAvailable) { parameters.Set("tmdbid", searchCriteria.TmdbId.Value.ToString()); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs index b1327a15a..751850af1 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabRssParser.cs @@ -74,16 +74,17 @@ namespace NzbDrone.Core.Indexers.Newznab protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases) { var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any()) { - _logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]); - } - else - { - _logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]); + _logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); + + return false; } + + _logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]); } return true; diff --git a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabSettings.cs index 5717d978b..2ca4362f6 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Newznab/NewznabSettings.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; @@ -43,7 +42,8 @@ namespace NzbDrone.Core.Indexers.Newznab RuleFor(c => c.VipExpiration).Must(c => c.IsFutureDate()) .When(c => c.VipExpiration.IsNotNullOrWhiteSpace()) - .WithMessage("Must be a future date"); + .WithMessage("Must be a future date") + .AsWarning(); } } @@ -60,22 +60,23 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(0, Label = "URL")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)] + [FieldDefinition(1, Label = "IndexerSettingsApiPath", HelpText = "IndexerSettingsApiPathHelpText", Advanced = true)] + [FieldToken(TokenField.HelpText, "IndexerSettingsApiPath", "url", "/api")] public string ApiPath { get; set; } - [FieldDefinition(2, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerNewznabSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)] + [FieldDefinition(5, Label = "IndexerSettingsAdditionalParameters", HelpText = "IndexerNewznabSettingsAdditionalParametersHelpText", Advanced = true)] public string AdditionalParameters { get; set; } - [FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")] + [FieldDefinition(6, Label = "IndexerSettingsVipExpiration", HelpText = "IndexerNewznabSettingsVipExpirationHelpText")] public string VipExpiration { get; set; } [FieldDefinition(7)] public IndexerBaseSettings BaseSettings { get; set; } = new (); - public List<IndexerCategory> Categories { get; set; } + public NewznabCapabilitiesSettings Capabilities { get; set; } // Field 8 is used by TorznabSettings MinimumSeeders // If you need to add another field here, update TorznabSettings as well and this comment diff --git a/src/NzbDrone.Core/Indexers/Definitions/NorBits.cs b/src/NzbDrone.Core/Indexers/Definitions/NorBits.cs index ddfff26cf..e989a5c6a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/NorBits.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/NorBits.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; -using System.Net.Http; using System.Text; using System.Threading.Tasks; using AngleSharp.Dom; @@ -28,7 +27,7 @@ public class NorBits : TorrentIndexerBase<NorBitsSettings> public override string[] IndexerUrls => new[] { "https://norbits.net/" }; public override string Description => "NorBits is a Norwegian Private site for MOVIES / TV / GENERAL"; public override string Language => "nb-NO"; - public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1"); + public override Encoding Encoding => Encoding.UTF8; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override IndexerCapabilities Capabilities => SetCapabilities(); @@ -76,12 +75,16 @@ public class NorBits : TorrentIndexerBase<NorBitsSettings> { LogResponseContent = true, AllowAutoRedirect = true, - Method = HttpMethod.Post + SuppressHttpError = true }; var authLoginCheckRequest = requestBuilder3 + .Post() .AddFormParameter("username", Settings.Username) .AddFormParameter("password", Settings.Password) + .AddFormParameter("code", Settings.TwoFactorAuthCode ?? string.Empty) + .AddFormParameter("logout", "no") + .AddFormParameter("returnto", "/") .SetCookies(indexPage.GetCookies()) .SetHeader("Referer", loginUrl) .Build(); @@ -126,26 +129,14 @@ public class NorBits : TorrentIndexerBase<NorBitsSettings> } }; - caps.Categories.AddCategoryMapping("main_cat[]=1&sub2_cat[]=49", NewznabStandardCategory.MoviesUHD, "Filmer - UHD-2160p"); - caps.Categories.AddCategoryMapping("main_cat[]=1&sub2_cat[]=19", NewznabStandardCategory.MoviesHD, "Filmer - HD-1080p/i"); - caps.Categories.AddCategoryMapping("main_cat[]=1&sub2_cat[]=20", NewznabStandardCategory.MoviesHD, "Filmer - HD-720p"); - caps.Categories.AddCategoryMapping("main_cat[]=1&sub2_cat[]=22", NewznabStandardCategory.MoviesSD, "Filmer - SD"); - caps.Categories.AddCategoryMapping("main_cat[]=2&sub2_cat[]=49", NewznabStandardCategory.TVUHD, "TV - UHD-2160p"); - caps.Categories.AddCategoryMapping("main_cat[]=2&sub2_cat[]=19", NewznabStandardCategory.TVHD, "TV - HD-1080p/i"); - caps.Categories.AddCategoryMapping("main_cat[]=2&sub2_cat[]=20", NewznabStandardCategory.TVHD, "TV - HD-720p"); - caps.Categories.AddCategoryMapping("main_cat[]=2&sub2_cat[]=22", NewznabStandardCategory.TVSD, "TV - SD"); + caps.Categories.AddCategoryMapping("main_cat[]=1", NewznabStandardCategory.Movies, "Filmer"); + caps.Categories.AddCategoryMapping("main_cat[]=2", NewznabStandardCategory.TV, "TV"); caps.Categories.AddCategoryMapping("main_cat[]=3", NewznabStandardCategory.PC, "Programmer"); caps.Categories.AddCategoryMapping("main_cat[]=4", NewznabStandardCategory.Console, "Spill"); - caps.Categories.AddCategoryMapping("main_cat[]=5&sub2_cat[]=42", NewznabStandardCategory.AudioMP3, "Musikk - 192"); - caps.Categories.AddCategoryMapping("main_cat[]=5&sub2_cat[]=43", NewznabStandardCategory.AudioMP3, "Musikk - 256"); - caps.Categories.AddCategoryMapping("main_cat[]=5&sub2_cat[]=44", NewznabStandardCategory.AudioMP3, "Musikk - 320"); - caps.Categories.AddCategoryMapping("main_cat[]=5&sub2_cat[]=45", NewznabStandardCategory.AudioMP3, "Musikk - VBR"); - caps.Categories.AddCategoryMapping("main_cat[]=5&sub2_cat[]=46", NewznabStandardCategory.AudioLossless, "Musikk - Lossless"); + caps.Categories.AddCategoryMapping("main_cat[]=5", NewznabStandardCategory.Audio, "Musikk"); caps.Categories.AddCategoryMapping("main_cat[]=6", NewznabStandardCategory.Books, "Tidsskrift"); caps.Categories.AddCategoryMapping("main_cat[]=7", NewznabStandardCategory.AudioAudiobook, "Lydbøker"); - caps.Categories.AddCategoryMapping("main_cat[]=8&sub2_cat[]=19", NewznabStandardCategory.AudioVideo, "Musikkvideoer - HD-1080p/i"); - caps.Categories.AddCategoryMapping("main_cat[]=8&sub2_cat[]=20", NewznabStandardCategory.AudioVideo, "Musikkvideoer - HD-720p"); - caps.Categories.AddCategoryMapping("main_cat[]=8&sub2_cat[]=22", NewznabStandardCategory.AudioVideo, "Musikkvideoer - SD"); + caps.Categories.AddCategoryMapping("main_cat[]=8", NewznabStandardCategory.AudioVideo, "Musikkvideoer"); caps.Categories.AddCategoryMapping("main_cat[]=40", NewznabStandardCategory.AudioOther, "Podcasts"); return caps; @@ -174,6 +165,11 @@ public class NorBitsRequestGenerator : IIndexerRequestGenerator { "scenerelease", "0" } }; + if (_settings.FreeLeechOnly) + { + parameters.Set("FL", "1"); + } + var searchTerm = "search="; if (!string.IsNullOrWhiteSpace(imdbId)) @@ -182,7 +178,7 @@ public class NorBitsRequestGenerator : IIndexerRequestGenerator } else if (!string.IsNullOrWhiteSpace(term)) { - searchTerm = "search=" + term.UrlEncode(Encoding.GetEncoding(28591)); + searchTerm = "search=" + term.UrlEncode(Encoding.UTF8); } searchUrl += "?" + searchTerm + "&" + parameters.GetQueryString(); @@ -269,20 +265,17 @@ public class NorBitsParser : IParseIndexerResponse foreach (var row in rows) { - var link = _settings.BaseUrl + row.QuerySelector("td:nth-of-type(2) > a[href*=\"download.php?id=\"]")?.GetAttribute("href").TrimStart('/'); + var link = _settings.BaseUrl + row.QuerySelector("td:nth-of-type(2) > a[href*=\"download.php?id=\"]")?.GetAttribute("href")?.TrimStart('/'); var qDetails = row.QuerySelector("td:nth-of-type(2) > a[href*=\"details.php?id=\"]"); - var title = qDetails?.GetAttribute("title").Trim(); - var details = _settings.BaseUrl + qDetails?.GetAttribute("href").TrimStart('/'); + var title = qDetails?.GetAttribute("title")?.Trim(); + var details = _settings.BaseUrl + qDetails?.GetAttribute("href")?.TrimStart('/'); - var mainCategory = row.QuerySelector("td:nth-of-type(1) > div > a[href*=\"main_cat[]\"]")?.GetAttribute("href")?.Split('?').Last(); - var secondCategory = row.QuerySelector("td:nth-of-type(1) > div > a[href*=\"sub2_cat[]\"]")?.GetAttribute("href")?.Split('?').Last(); + var catQuery = row.QuerySelector("td:nth-of-type(1) a[href*=\"main_cat[]\"]")?.GetAttribute("href")?.Split('?').Last().Split('&', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var category = catQuery?.FirstOrDefault(x => x.StartsWith("main_cat[]=", StringComparison.OrdinalIgnoreCase)); - var categoryList = new[] { mainCategory, secondCategory }; - var cat = string.Join("&", categoryList.Where(c => !string.IsNullOrWhiteSpace(c))); - - var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(9)").TextContent); - var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(10)").TextContent); + var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(9)")?.TextContent); + var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(10)")?.TextContent); var release = new TorrentInfo { @@ -290,7 +283,7 @@ public class NorBitsParser : IParseIndexerResponse InfoUrl = details, DownloadUrl = link, Title = title, - Categories = _categories.MapTrackerCatToNewznab(cat), + Categories = _categories.MapTrackerCatToNewznab(category), Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(7)")?.TextContent), Files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(3) > a")?.TextContent.Trim()), Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(8)")?.FirstChild?.TextContent.Trim()), @@ -311,8 +304,8 @@ public class NorBitsParser : IParseIndexerResponse release.Genres = genres.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); } - var imdbLink = row.QuerySelector("a[href*=\"imdb.com/title/tt\"]")?.GetAttribute("href"); - release.ImdbId = ParseUtil.GetImdbId(imdbLink) ?? 0; + var imdbId = ParseUtil.GetImdbId(row.QuerySelector("a[href*=\"imdb.com/title/tt\"]")?.GetAttribute("href")?.TrimEnd('/')?.Split('/')?.LastOrDefault()); + release.ImdbId = imdbId ?? 0; if (row.QuerySelector("img[title=\"100% freeleech\"]") != null) { @@ -343,6 +336,12 @@ public class NorBitsSettings : UserPassTorrentBaseSettings UseFullSearch = false; } - [FieldDefinition(4, Label = "Use Full Search", HelpText = "Use Full Search from Site", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the NorBits Web Site. Otherwise just leave it empty.")] + public string TwoFactorAuthCode { get; set; } + + [FieldDefinition(5, Label = "Use Full Search", Type = FieldType.Checkbox, HelpText = "Use Full Search from Site")] public bool UseFullSearch { get; set; } + + [FieldDefinition(6, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search FreeLeech torrents only")] + public bool FreeLeechOnly { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs b/src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs index bf57c1ca3..58c61ef77 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs @@ -20,6 +20,7 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Definitions { + [Obsolete("Site has removed API access.")] public class NzbIndex : UsenetIndexerBase<NzbIndexSettings> { public override string Name => "NZBIndex"; @@ -1187,10 +1188,10 @@ namespace NzbDrone.Core.Indexers.Definitions ApiKey = ""; } - [FieldDefinition(1, Label = "Base Url", HelpText = "Select which baseurl Prowlarr will use for requests to the site", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls")] + [FieldDefinition(1, Label = "IndexerSettingsBaseUrl", HelpText = "IndexerSettingsBaseUrlHelpText", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Site API Key")] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerNzbIndexSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } [FieldDefinition(3)] diff --git a/src/NzbDrone.Core/Indexers/Definitions/Orpheus.cs b/src/NzbDrone.Core/Indexers/Definitions/Orpheus.cs index c8513296e..7075e170b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Orpheus.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Orpheus.cs @@ -8,6 +8,7 @@ using FluentValidation; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Definitions.Gazelle; @@ -50,6 +51,20 @@ namespace NzbDrone.Core.Indexers.Definitions return new OrpheusParser(Settings, Capabilities.Categories); } + protected override Task<HttpRequest> GetDownloadRequest(Uri link) + { + var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri) + { + AllowAutoRedirect = FollowRedirect + }; + + var request = requestBuilder + .SetHeader("Authorization", $"token {Settings.Apikey}") + .Build(); + + return Task.FromResult(request); + } + protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria) { var cleanReleases = base.CleanupReleases(releases, searchCriteria); @@ -89,46 +104,30 @@ namespace NzbDrone.Core.Indexers.Definitions return caps; } - public override async Task<byte[]> Download(Uri link) + public override async Task<IndexerDownloadResponse> Download(Uri link) { - var request = new HttpRequestBuilder(link.AbsoluteUri) - .SetHeader("Authorization", $"token {Settings.Apikey}") - .Build(); + var downloadResponse = await base.Download(link); - var downloadBytes = Array.Empty<byte>(); + var fileData = downloadResponse.Data; - try + if (Settings.UseFreeleechToken == (int)OrpheusFreeleechTokenAction.Preferred + && fileData.Length >= 1 + && fileData[0] != 'd' // simple test for torrent vs HTML content + && link.Query.Contains("usetoken=1")) { - var response = await _httpClient.ExecuteProxiedAsync(request, Definition); - downloadBytes = response.ResponseData; + var html = Encoding.GetString(fileData); - if (downloadBytes.Length >= 1 - && downloadBytes[0] != 'd' // simple test for torrent vs HTML content - && link.Query.Contains("usetoken=1")) + if (html.Contains("You do not have any freeleech tokens left.") + || html.Contains("You do not have enough freeleech tokens") + || html.Contains("This torrent is too large.") + || html.Contains("You cannot use tokens here")) { - var html = Encoding.GetString(downloadBytes); - if (html.Contains("You do not have any freeleech tokens left.") - || html.Contains("You do not have enough freeleech tokens") - || html.Contains("This torrent is too large.") - || html.Contains("You cannot use tokens here")) - { - // download again without usetoken - request.Url = new HttpUri(link.ToString().Replace("&usetoken=1", "")); - - response = await _httpClient.ExecuteProxiedAsync(request, Definition); - downloadBytes = response.ResponseData; - } + // Try to download again without usetoken + downloadResponse = await base.Download(link.RemoveQueryParam("usetoken")); } } - catch (Exception) - { - _indexerStatusService.RecordFailure(Definition.Id); - _logger.Error("Download failed"); - } - ValidateDownloadData(downloadBytes); - - return downloadBytes; + return downloadResponse; } } @@ -254,7 +253,9 @@ namespace NzbDrone.Core.Indexers.Definitions if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) @@ -276,16 +277,23 @@ namespace NzbDrone.Core.Indexers.Definitions { foreach (var torrent in result.Torrents) { + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (_settings.UseFreeleechToken == (int)OrpheusFreeleechTokenAction.Required && !torrent.CanUseToken) + { + continue; + } + var id = torrent.TorrentId; var title = GetTitle(result, torrent); var infoUrl = GetInfoUrl(result.GroupId, id); + var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech; var release = new TorrentInfo { Guid = infoUrl, InfoUrl = infoUrl, - DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken), + DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech), Title = WebUtility.HtmlDecode(title), Artist = WebUtility.HtmlDecode(result.Artist), Album = WebUtility.HtmlDecode(result.GroupName), @@ -299,7 +307,7 @@ namespace NzbDrone.Core.Indexers.Definitions Scene = torrent.Scene, Files = torrent.FileCount, Grabs = torrent.Snatches, - DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1 }; @@ -320,22 +328,29 @@ namespace NzbDrone.Core.Indexers.Definitions // Non-Audio files are formatted a little differently (1:1 for group and torrents) else { + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (_settings.UseFreeleechToken == (int)OrpheusFreeleechTokenAction.Required && !result.CanUseToken) + { + continue; + } + var id = result.TorrentId; var infoUrl = GetInfoUrl(result.GroupId, id); + var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech; var release = new TorrentInfo { Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech), Title = WebUtility.HtmlDecode(result.GroupName), Size = long.Parse(result.Size), - DownloadUrl = GetDownloadUrl(id, result.CanUseToken), - InfoUrl = infoUrl, Seeders = int.Parse(result.Seeders), Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders), PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime(result.GroupTime), Files = result.FileCount, Grabs = result.Snatches, - DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1 }; @@ -362,7 +377,7 @@ namespace NzbDrone.Core.Indexers.Definitions private string GetTitle(GazelleRelease result, GazelleTorrent torrent) { - var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]"; + var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})"; if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown") { @@ -374,14 +389,23 @@ namespace NzbDrone.Core.Indexers.Definitions title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]"; } - title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]"; + var flags = new List<string> + { + $"{torrent.Format} {torrent.Encoding}", + $"{torrent.Media}" + }; + + if (torrent.HasLog) + { + flags.Add("Log (" + torrent.LogScore + "%)"); + } if (torrent.HasCue) { - title += " [Cue]"; + flags.Add("Cue"); } - return title; + return $"{title} [{string.Join(" / ", flags)}]"; } private string GetDownloadUrl(int torrentId, bool canUseToken) @@ -392,7 +416,7 @@ namespace NzbDrone.Core.Indexers.Definitions .AddQueryParam("id", torrentId); // Orpheus fails to download if usetoken=0 so we need to only add if we will use one - if (_settings.UseFreeleechToken) + if (_settings.UseFreeleechToken is (int)OrpheusFreeleechTokenAction.Preferred or (int)OrpheusFreeleechTokenAction.Required && canUseToken) { url = url.AddQueryParam("usetoken", "1"); } @@ -426,18 +450,30 @@ namespace NzbDrone.Core.Indexers.Definitions public OrpheusSettings() { Apikey = ""; - UseFreeleechToken = false; + UseFreeleechToken = (int)OrpheusFreeleechTokenAction.Never; } - [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", HelpText = "IndexerOrpheusSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string Apikey { get; set; } - [FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)] - public bool UseFreeleechToken { get; set; } + [FieldDefinition(3, Type = FieldType.Select, Label = "Use Freeleech Tokens", SelectOptions = typeof(OrpheusFreeleechTokenAction), HelpText = "When to use freeleech tokens")] + public int UseFreeleechToken { get; set; } public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } + + public enum OrpheusFreeleechTokenAction + { + [FieldOption(Label = "Never", Hint = "Do not use tokens")] + Never = 0, + + [FieldOption(Label = "Preferred", Hint = "Use token if possible")] + Preferred = 1, + + [FieldOption(Label = "Required", Hint = "Abort download if unable to use token")] + Required = 2, + } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcorn.cs b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcorn.cs index 9cb147b48..7426157ab 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcorn.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcorn.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NLog; using NzbDrone.Core.Configuration; @@ -15,6 +16,7 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn public override bool SupportsSearch => true; public override bool SupportsPagination => true; public override int PageSize => 50; + public override TimeSpan RateLimit => TimeSpan.FromSeconds(4); public override IndexerCapabilities Capabilities => SetCapabilities(); @@ -73,6 +75,6 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn public class PassThePopcornFlag : IndexerFlag { public static IndexerFlag Golden => new ("golden", "Release follows Golden Popcorn quality rules"); - public static IndexerFlag Approved => new ("approved", "Release approved by PTP"); + public static IndexerFlag Approved => new ("approved", "Release approved by PTP staff"); } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornParser.cs b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornParser.cs index bc4d11463..7b6fe586e 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornParser.cs @@ -56,6 +56,19 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn { foreach (var torrent in result.Torrents) { + // skip non-freeleech results when freeleech only is set + var downloadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch + { + "FREELEECH" or "NEUTRAL LEECH" => 0, + "HALF LEECH" => 0.5, + _ => 1 + }; + + if (_settings.FreeleechOnly && downloadVolumeFactor != 0.0) + { + continue; + } + var id = torrent.Id; var title = torrent.ReleaseName; @@ -78,6 +91,12 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn categories.Add(NewznabStandardCategory.TV); } + var uploadVolumeFactor = torrent.FreeleechType?.ToUpperInvariant() switch + { + "NEUTRAL LEECH" => 0, + _ => 1 + }; + torrentInfos.Add(new TorrentInfo { Guid = $"PassThePopcorn-{id}", @@ -94,8 +113,8 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0, Scene = torrent.Scene, IndexerFlags = flags, - DownloadVolumeFactor = torrent.FreeleechType is "Freeleech" ? 0 : 1, - UploadVolumeFactor = 1, + DownloadVolumeFactor = downloadVolumeFactor, + UploadVolumeFactor = uploadVolumeFactor, MinimumRatio = 1, MinimumSeedTime = 345600, Genres = result.Tags ?? new List<string>(), diff --git a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornRequestGenerator.cs index 17d9b1813..90b370e7b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornRequestGenerator.cs @@ -86,6 +86,11 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn parameters.Set("freetorrent", "1"); } + if (_settings.GoldenPopcornOnly) + { + parameters.Set("scene", "2"); + } + var queryCats = _capabilities.Categories .MapTorznabCapsToTrackers(searchCriteria.Categories) .Select(int.Parse) diff --git a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornSettings.cs index 475dfa73b..5494ff989 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PassThePopcorn/PassThePopcornSettings.cs @@ -18,15 +18,18 @@ namespace NzbDrone.Core.Indexers.Definitions.PassThePopcorn { private static readonly PassThePopcornSettingsValidator Validator = new (); - [FieldDefinition(2, Label = "API User", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)] + [FieldDefinition(2, Label = "IndexerSettingsApiUser", HelpText = "IndexerPassThePopcornSettingsApiUserHelpText", Privacy = PrivacyLevel.UserName)] public string APIUser { get; set; } - [FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "ApiKey", HelpText = "IndexerPassThePopcornSettingsApiKeyHelpText", Privacy = PrivacyLevel.ApiKey)] public string APIKey { get; set; } - [FieldDefinition(4, Label = "Freeleech Only", HelpText = "Return only freeleech torrents", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "IndexerSettingsFreeleechOnly", HelpText = "IndexerPassThePopcornSettingsFreeleechOnlyHelpText", Type = FieldType.Checkbox)] public bool FreeleechOnly { get; set; } + [FieldDefinition(5, Label = "IndexerPassThePopcornSettingsGoldenPopcornOnly", HelpText = "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText", Type = FieldType.Checkbox, Advanced = true)] + public bool GoldenPopcornOnly { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Definitions/PirateTheNet.cs b/src/NzbDrone.Core/Indexers/Definitions/PirateTheNet.cs deleted file mode 100644 index 53adf8e30..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/PirateTheNet.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using AngleSharp.Html.Parser; -using Newtonsoft.Json; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions; -[Obsolete("PirateTheNet has shutdown 2023-10-14")] -public class PirateTheNet : TorrentIndexerBase<UserPassTorrentBaseSettings> -{ - public override string Name => "PirateTheNet"; - public override string[] IndexerUrls => new[] { "https://piratethenet.org/" }; - public override string[] LegacyUrls => new[] { "http://piratethenet.org/" }; - public override string Description => "PirateTheNet (PTN) is a ratioless movie tracker."; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override IndexerCapabilities Capabilities => SetCapabilities(); - private string LoginUrl => Settings.BaseUrl + "takelogin.php"; - private string CaptchaUrl => Settings.BaseUrl + "simpleCaptcha.php?numImages=1"; - - public PirateTheNet(IIndexerHttpClient httpClient, - IEventAggregator eventAggregator, - IIndexerStatusService indexerStatusService, - IConfigService configService, - Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new PirateTheNetRequestGenerator(Settings, Capabilities); - } - - public override IParseIndexerResponse GetParser() - { - return new PirateTheNetParser(Settings, Capabilities.Categories); - } - - protected override async Task DoLogin() - { - var captchaPage = await ExecuteAuth(new HttpRequest(CaptchaUrl)); - - var captchaResponse = JsonConvert.DeserializeAnonymousType(captchaPage.Content, new - { - images = new[] { new { hash = string.Empty } } - }); - - var requestBuilder = new HttpRequestBuilder(LoginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true, - Method = HttpMethod.Post - }; - - var authLoginRequest = requestBuilder - .SetCookies(captchaPage.GetCookies()) - .AddFormParameter("username", Settings.Username) - .AddFormParameter("password", Settings.Password) - .AddFormParameter("captchaSelection", captchaResponse.images[0].hash) - .SetHeader("Content-Type", "application/x-www-form-urlencoded") - .SetHeader("Referer", LoginUrl) - .Build(); - - var response = await ExecuteAuth(authLoginRequest); - - if (CheckIfLoginNeeded(response)) - { - throw new IndexerAuthException("Login Failed."); - } - - var cookies = response.GetCookies(); - UpdateCookies(cookies, DateTime.Now.AddDays(30)); - - _logger.Debug("Authentication succeeded."); - } - - protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) - { - return !httpResponse.Content.Contains("logout.php"); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - MovieSearchParams = new List<MovieSearchParam> - { - MovieSearchParam.Q, MovieSearchParam.ImdbId - } - }; - - caps.Categories.AddCategoryMapping("1080P", NewznabStandardCategory.MoviesHD, "1080P"); - caps.Categories.AddCategoryMapping("2160P", NewznabStandardCategory.MoviesHD, "2160P"); - caps.Categories.AddCategoryMapping("720P", NewznabStandardCategory.MoviesHD, "720P"); - caps.Categories.AddCategoryMapping("BDRip", NewznabStandardCategory.MoviesSD, "BDRip"); - caps.Categories.AddCategoryMapping("BluRay", NewznabStandardCategory.MoviesBluRay, "BluRay"); - caps.Categories.AddCategoryMapping("BRRip", NewznabStandardCategory.MoviesSD, "BRRip"); - caps.Categories.AddCategoryMapping("DVDR", NewznabStandardCategory.MoviesDVD, "DVDR"); - caps.Categories.AddCategoryMapping("DVDRip", NewznabStandardCategory.MoviesSD, "DVDRip"); - caps.Categories.AddCategoryMapping("FLAC", NewznabStandardCategory.AudioLossless, "FLAC OST"); - caps.Categories.AddCategoryMapping("MP3", NewznabStandardCategory.AudioMP3, "MP3 OST"); - caps.Categories.AddCategoryMapping("MP4", NewznabStandardCategory.MoviesOther, "MP4"); - caps.Categories.AddCategoryMapping("Packs", NewznabStandardCategory.MoviesOther, "Packs"); - caps.Categories.AddCategoryMapping("R5", NewznabStandardCategory.MoviesDVD, "R5 / SCR"); - caps.Categories.AddCategoryMapping("Remux", NewznabStandardCategory.MoviesOther, "Remux"); - caps.Categories.AddCategoryMapping("TVRip", NewznabStandardCategory.MoviesOther, "TVRip"); - caps.Categories.AddCategoryMapping("WebRip", NewznabStandardCategory.MoviesWEBDL, "WebRip"); - - return caps; - } -} - -public class PirateTheNetRequestGenerator : IIndexerRequestGenerator -{ - private readonly UserPassTorrentBaseSettings _settings; - private readonly IndexerCapabilities _capabilities; - - public PirateTheNetRequestGenerator(UserPassTorrentBaseSettings settings, IndexerCapabilities capabilities) - { - _settings = settings; - _capabilities = capabilities; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); - - return pageableRequests; - } - - private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null) - { - var parameters = new NameValueCollection - { - { "action", "torrentstable" }, - { "viewtype", "0" }, - { "visiblecategories", "Action,Adventure,Animation,Biography,Comedy,Crime,Documentary,Drama,Family,Fantasy,History,Horror,Kids,Music,Mystery,Packs,Romance,Sci-Fi,Short,Sports,Thriller,War,Western" }, - { "page", "1" }, - { "visibility", "showall" }, - { "compression", "showall" }, - { "sort", "added" }, - { "order", "DESC" }, - { "titleonly", "true" }, - { "packs", "showall" }, - { "bookmarks", "showall" }, - { "subscriptions", "showall" }, - { "skw", "showall" } - }; - - if (imdbId.IsNotNullOrWhiteSpace()) - { - parameters.Set("advancedsearchparameters", $"[imdb={imdbId}]"); - } - else if (term.IsNotNullOrWhiteSpace()) - { - parameters.Set("searchstring", term); - } - - var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories); - if (queryCats.Any()) - { - parameters.Set("hiddenqualities", string.Join(",", queryCats)); - } - - var searchUrl = _settings.BaseUrl + "torrentsutils.php"; - - if (parameters.Count > 0) - { - searchUrl += $"?{parameters.GetQueryString()}"; - } - - var request = new IndexerRequest(searchUrl, HttpAccept.Html); - - yield return request; - } - - public Func<IDictionary<string, string>> GetCookies { get; set; } - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } -} - -public class PirateTheNetParser : IParseIndexerResponse -{ - private readonly UserPassTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; - - public PirateTheNetParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) - { - _settings = settings; - _categories = categories; - } - - public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) - { - var releaseInfos = new List<ReleaseInfo>(); - - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(indexerResponse.Content); - - var rows = dom.QuerySelectorAll("table.main > tbody > tr"); - foreach (var row in rows.Skip(1)) - { - var qDetails = row.QuerySelector("td:nth-of-type(2) > a:nth-of-type(1)"); - var title = qDetails?.GetAttribute("alt")?.Trim(); - - var infoUrl = _settings.BaseUrl + qDetails?.GetAttribute("href")?.TrimStart('/'); - var downloadUrl = _settings.BaseUrl + row.QuerySelector("td > a:has(img[alt=\"Download Torrent\"])")?.GetAttribute("href")?.TrimStart('/'); - - var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(9)")?.TextContent); - var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(10)")?.TextContent); - - var cat = row.QuerySelector("td:nth-of-type(1) > a > img")?.GetAttribute("src")?.Split('/').Last().Split('.').First() ?? "packs"; - - var release = new TorrentInfo - { - Guid = infoUrl, - InfoUrl = infoUrl, - DownloadUrl = downloadUrl, - Title = title, - Categories = _categories.MapTrackerCatToNewznab(cat), - Seeders = seeders, - Peers = seeders + leechers, - Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(7)")?.TextContent.Trim()), - Files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(4)")?.TextContent), - Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)")?.TextContent), - DownloadVolumeFactor = 0, // ratioless - UploadVolumeFactor = 1, - MinimumRatio = 1, - MinimumSeedTime = 259200, // 72 hours - }; - - var added = row.QuerySelector("td:nth-of-type(6) > nobr")?.TextContent.Trim(); - if (added.StartsWith("Today ")) - { - release.PublishDate = DateTime.Now.Date + DateTime.ParseExact(added.Split(" ", 2).Last(), "hh:mm tt", CultureInfo.InvariantCulture).TimeOfDay; - } - else if (added.StartsWith("Yesterday ")) - { - release.PublishDate = DateTime.Now.AddDays(-1).Date + DateTime.ParseExact(added.Split(" ", 2).Last(), "hh:mm tt", CultureInfo.InvariantCulture).TimeOfDay; - } - else - { - release.PublishDate = DateTime.ParseExact(added, "MMM d yyyy hh:mm tt", CultureInfo.InvariantCulture); - } - - releaseInfos.Add(release); - } - - return releaseInfos.ToArray(); - } - - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/PixelHD.cs b/src/NzbDrone.Core/Indexers/Definitions/PixelHD.cs index 1c78174ca..98549f370 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PixelHD.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PixelHD.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.Linq; using System.Net; using System.Text; using AngleSharp.Html.Parser; @@ -178,8 +179,7 @@ public class PixelHDParser : IParseIndexerResponse { var groupName = group.QuerySelector("strong:has(a[title=\"View Torrent\"])")?.TextContent.Replace(" ]", "]"); - var imdbLink = group.QuerySelector("a[href*=\"imdb.com/title/tt\"]")?.GetAttribute("href"); - var imdbId = ParseUtil.GetImdbId(imdbLink) ?? 0; + var imdbId = ParseUtil.GetImdbId(group.QuerySelector("a[href*=\"imdb.com/title/tt\"]")?.GetAttribute("href")?.TrimEnd('/')?.Split('/')?.LastOrDefault()) ?? 0; var rows = group.QuerySelectorAll("tr.group_torrent:has(a[href^=\"torrents.php?id=\"])"); foreach (var row in rows) diff --git a/src/NzbDrone.Core/Indexers/Definitions/Redacted.cs b/src/NzbDrone.Core/Indexers/Definitions/Redacted.cs index b54bf4199..963bb5a2a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Redacted.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Redacted.cs @@ -8,6 +8,7 @@ using FluentValidation; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Definitions.Gazelle; @@ -24,7 +25,8 @@ namespace NzbDrone.Core.Indexers.Definitions public class Redacted : TorrentIndexerBase<RedactedSettings> { public override string Name => "Redacted"; - public override string[] IndexerUrls => new[] { "https://redacted.ch/" }; + public override string[] IndexerUrls => new[] { "https://redacted.sh/" }; + public override string[] LegacyUrls => new[] { "https://redacted.ch/" }; public override string Description => "REDActed (Aka.PassTheHeadPhones) is one of the most well-known music trackers."; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override IndexerCapabilities Capabilities => SetCapabilities(); @@ -102,6 +104,32 @@ namespace NzbDrone.Core.Indexers.Definitions return caps; } + + public override async Task<IndexerDownloadResponse> Download(Uri link) + { + var downloadResponse = await base.Download(link); + + var fileData = downloadResponse.Data; + + if (Settings.UseFreeleechToken == (int)RedactedFreeleechTokenAction.Preferred + && fileData.Length >= 1 + && fileData[0] != 'd' // simple test for torrent vs HTML content + && link.Query.Contains("usetoken=1")) + { + var html = Encoding.GetString(fileData); + + if (html.Contains("You do not have any freeleech tokens left.") + || html.Contains("You do not have enough freeleech tokens") + || html.Contains("This torrent is too large.") + || html.Contains("You cannot use tokens here")) + { + // Try to download again without usetoken + downloadResponse = await base.Download(link.RemoveQueryParam("usetoken")); + } + } + + return downloadResponse; + } } public class RedactedRequestGenerator : IIndexerRequestGenerator @@ -193,6 +221,11 @@ namespace NzbDrone.Core.Indexers.Definitions queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1")); } + if (_settings.FreeloadOnly) + { + parameters.Set("freetorrent", "4"); + } + var searchUrl = _settings.BaseUrl.TrimEnd('/') + $"/ajax.php?{parameters.GetQueryString()}"; var request = new IndexerRequest(searchUrl, HttpAccept.Json); @@ -220,7 +253,9 @@ namespace NzbDrone.Core.Indexers.Definitions if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) @@ -242,16 +277,29 @@ namespace NzbDrone.Core.Indexers.Definitions { foreach (var torrent in result.Torrents) { + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (_settings.UseFreeleechToken == (int)RedactedFreeleechTokenAction.Required && !torrent.CanUseToken) + { + continue; + } + + // skip non-freeload results when freeload only is set + if (_settings.FreeloadOnly && !torrent.IsFreeload) + { + continue; + } + var id = torrent.TorrentId; var title = GetTitle(result, torrent); var infoUrl = GetInfoUrl(result.GroupId, id); + var isFreeLeech = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech; var release = new TorrentInfo { Guid = infoUrl, InfoUrl = infoUrl, - DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken), + DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken && !isFreeLeech), Title = WebUtility.HtmlDecode(title), Artist = WebUtility.HtmlDecode(result.Artist), Album = WebUtility.HtmlDecode(result.GroupName), @@ -265,7 +313,7 @@ namespace NzbDrone.Core.Indexers.Definitions Scene = torrent.Scene, Files = torrent.FileCount, Grabs = torrent.Snatches, - DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = torrent.IsNeutralLeech || torrent.IsFreeload ? 0 : 1 }; @@ -286,22 +334,35 @@ namespace NzbDrone.Core.Indexers.Definitions // Non-Audio files are formatted a little differently (1:1 for group and torrents) else { + // skip releases that cannot be used with freeleech tokens when the option is enabled + if (_settings.UseFreeleechToken == (int)RedactedFreeleechTokenAction.Required && !result.CanUseToken) + { + continue; + } + + // skip non-freeload results when freeload only is set + if (_settings.FreeloadOnly && !result.IsFreeload) + { + continue; + } + var id = result.TorrentId; var infoUrl = GetInfoUrl(result.GroupId, id); + var isFreeLeech = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech; var release = new TorrentInfo { Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id, result.CanUseToken && !isFreeLeech), Title = WebUtility.HtmlDecode(result.GroupName), Size = long.Parse(result.Size), - DownloadUrl = GetDownloadUrl(id, result.CanUseToken), - InfoUrl = infoUrl, Seeders = int.Parse(result.Seeders), Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders), PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime, Files = result.FileCount, Grabs = result.Snatches, - DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsFreeload || result.IsPersonalFreeLeech ? 0 : 1, + DownloadVolumeFactor = isFreeLeech ? 0 : 1, UploadVolumeFactor = result.IsNeutralLeech || result.IsFreeload ? 0 : 1 }; @@ -328,7 +389,7 @@ namespace NzbDrone.Core.Indexers.Definitions private string GetTitle(GazelleRelease result, GazelleTorrent torrent) { - var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]"; + var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})"; if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown") { @@ -340,14 +401,23 @@ namespace NzbDrone.Core.Indexers.Definitions title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]"; } - title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]"; + var flags = new List<string> + { + $"{torrent.Format} {torrent.Encoding}", + $"{torrent.Media}" + }; + + if (torrent.HasLog) + { + flags.Add("Log (" + torrent.LogScore + "%)"); + } if (torrent.HasCue) { - title += " [Cue]"; + flags.Add("Cue"); } - return title; + return $"{title} [{string.Join(" / ", flags)}]"; } private string GetDownloadUrl(int torrentId, bool canUseToken) @@ -357,7 +427,7 @@ namespace NzbDrone.Core.Indexers.Definitions .AddQueryParam("action", "download") .AddQueryParam("id", torrentId); - if (_settings.UseFreeleechToken && canUseToken) + if (_settings.UseFreeleechToken is (int)RedactedFreeleechTokenAction.Preferred or (int)RedactedFreeleechTokenAction.Required && canUseToken) { url = url.AddQueryParam("usetoken", "1"); } @@ -391,18 +461,33 @@ namespace NzbDrone.Core.Indexers.Definitions public RedactedSettings() { Apikey = ""; - UseFreeleechToken = false; + UseFreeleechToken = (int)RedactedFreeleechTokenAction.Never; } - [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "IndexerRedactedSettingsApiKeyHelpText")] public string Apikey { get; set; } - [FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)] - public bool UseFreeleechToken { get; set; } + [FieldDefinition(3, Type = FieldType.Select, Label = "Use Freeleech Tokens", SelectOptions = typeof(RedactedFreeleechTokenAction), HelpText = "When to use freeleech tokens")] + public int UseFreeleechToken { get; set; } + + [FieldDefinition(4, Label = "Freeload Only", Type = FieldType.Checkbox, Advanced = true, HelpTextWarning = "Search freeload torrents only. End date: 31 January 2024, 23:59 UTC.")] + public bool FreeloadOnly { get; set; } public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); } } + + public enum RedactedFreeleechTokenAction + { + [FieldOption(Label = "Never", Hint = "Do not use tokens")] + Never = 0, + + [FieldOption(Label = "Preferred", Hint = "Use token if possible")] + Preferred = 1, + + [FieldOption(Label = "Required", Hint = "Abort download if unable to use token")] + Required = 2, + } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/RuTracker.cs b/src/NzbDrone.Core/Indexers/Definitions/RuTracker.cs index 648062479..44f6bae38 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/RuTracker.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/RuTracker.cs @@ -23,14 +23,14 @@ namespace NzbDrone.Core.Indexers.Definitions { public class RuTracker : TorrentIndexerBase<RuTrackerSettings> { - public override string Name => "RuTracker"; + public override string Name => "RuTracker.org"; public override string[] IndexerUrls => new[] { "https://rutracker.org/", "https://rutracker.net/", "https://rutracker.nl/" }; - public override string Description => "RuTracker is a Semi-Private Russian torrent site with a thriving file-sharing community"; + public override string Description => "RuTracker.org is a RUSSIAN Semi-Private site with a thriving file-sharing community"; public override string Language => "ru-RU"; public override Encoding Encoding => Encoding.GetEncoding("windows-1251"); public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPrivate; @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Indexers.Definitions return new RuTrackerParser(Settings, Capabilities.Categories); } - public override async Task<byte[]> Download(Uri link) + public override async Task<IndexerDownloadResponse> Download(Uri link) { if (Settings.UseMagnetLinks && link.PathAndQuery.Contains("viewtopic.php?t=")) { @@ -144,6 +144,7 @@ namespace NzbDrone.Core.Indexers.Definitions SupportsRawSearch = true }; + // Note: When refreshing the categories use the tracker.php page and NOT the search.php page! caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.Movies, "Наше кино"); caps.Categories.AddCategoryMapping(941, NewznabStandardCategory.Movies, "|- Кино СССР"); caps.Categories.AddCategoryMapping(1666, NewznabStandardCategory.Movies, "|- Детские отечественные фильмы"); @@ -157,8 +158,8 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(2092, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2006-2010"); caps.Categories.AddCategoryMapping(2093, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2011-2015"); caps.Categories.AddCategoryMapping(2200, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2016-2020"); - caps.Categories.AddCategoryMapping(1950, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2021-2022"); - caps.Categories.AddCategoryMapping(252, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2023"); + caps.Categories.AddCategoryMapping(1950, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2021-2023"); + caps.Categories.AddCategoryMapping(252, NewznabStandardCategory.MoviesForeign, "|- Фильмы 2024"); caps.Categories.AddCategoryMapping(2540, NewznabStandardCategory.MoviesForeign, "|- Фильмы Ближнего Зарубежья"); caps.Categories.AddCategoryMapping(934, NewznabStandardCategory.MoviesForeign, "|- Азиатские фильмы"); caps.Categories.AddCategoryMapping(505, NewznabStandardCategory.MoviesForeign, "|- Индийское кино"); @@ -172,6 +173,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(709, NewznabStandardCategory.MoviesOther, "|- Документальные фильмы (Арт-хаус и авторское кино)"); caps.Categories.AddCategoryMapping(1577, NewznabStandardCategory.MoviesOther, "|- Анимация (Арт-хаус и авторское кино)"); caps.Categories.AddCategoryMapping(511, NewznabStandardCategory.TVOther, "Театр"); + caps.Categories.AddCategoryMapping(1493, NewznabStandardCategory.TVOther, "|- Спектакли без перевода"); caps.Categories.AddCategoryMapping(93, NewznabStandardCategory.MoviesDVD, "DVD Video"); caps.Categories.AddCategoryMapping(905, NewznabStandardCategory.MoviesDVD, "|- Классика мирового кинематографа (DVD Video)"); caps.Categories.AddCategoryMapping(101, NewznabStandardCategory.MoviesDVD, "|- Зарубежное кино (DVD Video)"); @@ -212,6 +214,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(521, NewznabStandardCategory.MoviesDVD, "|- Иностранные мультфильмы (DVD)"); caps.Categories.AddCategoryMapping(208, NewznabStandardCategory.Movies, "|- Отечественные мультфильмы"); caps.Categories.AddCategoryMapping(539, NewznabStandardCategory.Movies, "|- Отечественные полнометражные мультфильмы"); + caps.Categories.AddCategoryMapping(2183, NewznabStandardCategory.MoviesForeign, "|- Мультфильмы Ближнего Зарубежья"); caps.Categories.AddCategoryMapping(209, NewznabStandardCategory.MoviesForeign, "|- Иностранные мультфильмы"); caps.Categories.AddCategoryMapping(484, NewznabStandardCategory.MoviesForeign, "|- Иностранные короткометражные мультфильмы"); caps.Categories.AddCategoryMapping(822, NewznabStandardCategory.Movies, "|- Сборники мультфильмов"); @@ -391,19 +394,21 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(114, NewznabStandardCategory.TVOther, "|- [Видео Юмор] Сатирики и юмористы"); caps.Categories.AddCategoryMapping(1332, NewznabStandardCategory.TVOther, "|- Юмористические аудиопередачи"); caps.Categories.AddCategoryMapping(1495, NewznabStandardCategory.TVOther, "|- Аудио и видео ролики (Приколы и юмор)"); - caps.Categories.AddCategoryMapping(1392, NewznabStandardCategory.TVSport, "XXXII Летние Олимпийские игры 2020"); - caps.Categories.AddCategoryMapping(2475, NewznabStandardCategory.TVSport, "|- Легкая атлетика"); - caps.Categories.AddCategoryMapping(2493, NewznabStandardCategory.TVSport, "|- Плавание. Прыжки в воду. Синхронное плавание"); - caps.Categories.AddCategoryMapping(2113, NewznabStandardCategory.TVSport, "|- Спортивная гимнастика. Художественная гимнастика. Прыжки на батуте"); - caps.Categories.AddCategoryMapping(2482, NewznabStandardCategory.TVSport, "|- Велоспорт"); - caps.Categories.AddCategoryMapping(2103, NewznabStandardCategory.TVSport, "|- Академическая гребля. Гребля на байдарках и каноэ"); - caps.Categories.AddCategoryMapping(2522, NewznabStandardCategory.TVSport, "|- Бокс. Борьба Вольная и Греко-римская. Дзюдо. Карате. Тхэквондо"); - caps.Categories.AddCategoryMapping(2485, NewznabStandardCategory.TVSport, "|- Футбол"); - caps.Categories.AddCategoryMapping(2486, NewznabStandardCategory.TVSport, "|- Баскетбол. Волейбол. Гандбол. Водное поло. Регби. Хоккей на траве"); + caps.Categories.AddCategoryMapping(1346, NewznabStandardCategory.TVSport, "XXXIII Летние Олимпийские игры 2024"); + caps.Categories.AddCategoryMapping(2493, NewznabStandardCategory.TVSport, "|- Легкая атлетика. Плавание. Прыжки в воду. Синхронное плавание. Гим.."); + caps.Categories.AddCategoryMapping(2103, NewznabStandardCategory.TVSport, "|- Велоспорт. Академическая гребля. Гребля на байдарках и каноэ"); + caps.Categories.AddCategoryMapping(2485, NewznabStandardCategory.TVSport, "|- Футбол. Баскетбол. Волейбол. Гандбол. Водное поло. Регби. Хоккей н.."); caps.Categories.AddCategoryMapping(2479, NewznabStandardCategory.TVSport, "|- Теннис. Настольный теннис. Бадминтон"); caps.Categories.AddCategoryMapping(2089, NewznabStandardCategory.TVSport, "|- Фехтование. Стрельба. Стрельба из лука. Современное пятиборье"); + caps.Categories.AddCategoryMapping(2338, NewznabStandardCategory.TVSport, "|- Бокс. Борьба Вольная и Греко-римская. Дзюдо. Карате. Тхэквондо"); + caps.Categories.AddCategoryMapping(927, NewznabStandardCategory.TVSport, "|- Другие виды спорта"); + caps.Categories.AddCategoryMapping(1392, NewznabStandardCategory.TVSport, "XXXII Летние Олимпийские игры 2020"); + caps.Categories.AddCategoryMapping(2475, NewznabStandardCategory.TVSport, "|- Легкая атлетика. Плавание. Прыжки в воду. Синхронное плавание"); + caps.Categories.AddCategoryMapping(2113, NewznabStandardCategory.TVSport, "|- Гимнастика. Прыжки на батуте. Фехтование. Стрельба. Современное пя.."); + caps.Categories.AddCategoryMapping(2482, NewznabStandardCategory.TVSport, "|- Велоспорт. Академическая гребля. Гребля на байдарках и каноэ"); + caps.Categories.AddCategoryMapping(2522, NewznabStandardCategory.TVSport, "|- Бокс. Борьба Вольная и Греко-римская. Дзюдо. Карате. Тхэквондо"); + caps.Categories.AddCategoryMapping(2486, NewznabStandardCategory.TVSport, "|- Баскетбол. Волейбол. Гандбол. Водное поло. Регби. Хоккей на траве"); caps.Categories.AddCategoryMapping(1794, NewznabStandardCategory.TVSport, "|- Другие виды спорта"); - caps.Categories.AddCategoryMapping(2338, NewznabStandardCategory.TVSport, "|- Обзорные и аналитические программы"); caps.Categories.AddCategoryMapping(1315, NewznabStandardCategory.TVSport, "XXIV Зимние Олимпийские игры 2022"); caps.Categories.AddCategoryMapping(1336, NewznabStandardCategory.TVSport, "|- Биатлон"); caps.Categories.AddCategoryMapping(2171, NewznabStandardCategory.TVSport, "|- Лыжные гонки"); @@ -418,8 +423,8 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(255, NewznabStandardCategory.TVSport, "Спортивные турниры, фильмы и передачи"); caps.Categories.AddCategoryMapping(256, NewznabStandardCategory.TVSport, "|- Автоспорт"); caps.Categories.AddCategoryMapping(1986, NewznabStandardCategory.TVSport, "|- Мотоспорт"); - caps.Categories.AddCategoryMapping(660, NewznabStandardCategory.TVSport, "|- Формула-1 (2022)"); - caps.Categories.AddCategoryMapping(1551, NewznabStandardCategory.TVSport, "|- Формула-1 (2012-2021)"); + caps.Categories.AddCategoryMapping(660, NewznabStandardCategory.TVSport, "|- Формула-1 (2024)"); + caps.Categories.AddCategoryMapping(1551, NewznabStandardCategory.TVSport, "|- Формула-1 (2012-2023)"); caps.Categories.AddCategoryMapping(626, NewznabStandardCategory.TVSport, "|- Формула 1 (до 2011 вкл.)"); caps.Categories.AddCategoryMapping(262, NewznabStandardCategory.TVSport, "|- Велоспорт"); caps.Categories.AddCategoryMapping(1326, NewznabStandardCategory.TVSport, "|- Волейбол/Гандбол"); @@ -443,16 +448,16 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1319, NewznabStandardCategory.TVSport, "|- Спорт (видео)"); caps.Categories.AddCategoryMapping(1608, NewznabStandardCategory.TVSport, "⚽ Футбол"); caps.Categories.AddCategoryMapping(2294, NewznabStandardCategory.TVSport, "|- UHDTV"); - caps.Categories.AddCategoryMapping(1229, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2022 (финальный турнир)"); - caps.Categories.AddCategoryMapping(1693, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2022 (отбор)"); - caps.Categories.AddCategoryMapping(2532, NewznabStandardCategory.TVSport, "|- Чемпионат Европы 2020 [2021] (финальный турнир)"); + caps.Categories.AddCategoryMapping(1693, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2026 (отбор)"); caps.Categories.AddCategoryMapping(136, NewznabStandardCategory.TVSport, "|- Чемпионат Европы 2024 (отбор)"); + caps.Categories.AddCategoryMapping(2532, NewznabStandardCategory.TVSport, "|- Чемпионат Европы 2020 [2021] (финальный турнир)"); caps.Categories.AddCategoryMapping(592, NewznabStandardCategory.TVSport, "|- Лига Наций"); + caps.Categories.AddCategoryMapping(1229, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2022"); caps.Categories.AddCategoryMapping(2533, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2018 (игры)"); caps.Categories.AddCategoryMapping(1952, NewznabStandardCategory.TVSport, "|- Чемпионат Мира 2018 (обзорные передачи, документалистика)"); caps.Categories.AddCategoryMapping(1621, NewznabStandardCategory.TVSport, "|- Чемпионаты Мира"); - caps.Categories.AddCategoryMapping(2075, NewznabStandardCategory.TVSport, "|- Россия 2022-2023"); - caps.Categories.AddCategoryMapping(1668, NewznabStandardCategory.TVSport, "|- Россия 2021-2022"); + caps.Categories.AddCategoryMapping(2075, NewznabStandardCategory.TVSport, "|- Россия 2024-2025"); + caps.Categories.AddCategoryMapping(1668, NewznabStandardCategory.TVSport, "|- Россия 2023-2024"); caps.Categories.AddCategoryMapping(1613, NewznabStandardCategory.TVSport, "|- Россия/СССР"); caps.Categories.AddCategoryMapping(1614, NewznabStandardCategory.TVSport, "|- Англия"); caps.Categories.AddCategoryMapping(1623, NewznabStandardCategory.TVSport, "|- Испания"); @@ -462,13 +467,13 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(2514, NewznabStandardCategory.TVSport, "|- Украина"); caps.Categories.AddCategoryMapping(1616, NewznabStandardCategory.TVSport, "|- Другие национальные чемпионаты и кубки"); caps.Categories.AddCategoryMapping(2014, NewznabStandardCategory.TVSport, "|- Международные турниры"); - caps.Categories.AddCategoryMapping(1442, NewznabStandardCategory.TVSport, "|- Еврокубки 2022-2023"); - caps.Categories.AddCategoryMapping(1491, NewznabStandardCategory.TVSport, "|- Еврокубки 2021-2022"); - caps.Categories.AddCategoryMapping(1987, NewznabStandardCategory.TVSport, "|- Еврокубки 2011-2021"); + caps.Categories.AddCategoryMapping(1442, NewznabStandardCategory.TVSport, "|- Еврокубки 2024-2025"); + caps.Categories.AddCategoryMapping(1491, NewznabStandardCategory.TVSport, "|- Еврокубки 2023-2024"); + caps.Categories.AddCategoryMapping(1987, NewznabStandardCategory.TVSport, "|- Еврокубки 2011-2023"); caps.Categories.AddCategoryMapping(1617, NewznabStandardCategory.TVSport, "|- Еврокубки"); caps.Categories.AddCategoryMapping(1620, NewznabStandardCategory.TVSport, "|- Чемпионаты Европы"); caps.Categories.AddCategoryMapping(1998, NewznabStandardCategory.TVSport, "|- Товарищеские турниры и матчи"); - caps.Categories.AddCategoryMapping(1343, NewznabStandardCategory.TVSport, "|- Обзорные и аналитические передачи 2018-2022"); + caps.Categories.AddCategoryMapping(1343, NewznabStandardCategory.TVSport, "|- Обзорные и аналитические передачи 2018-2023"); caps.Categories.AddCategoryMapping(751, NewznabStandardCategory.TVSport, "|- Обзорные и аналитические передачи"); caps.Categories.AddCategoryMapping(497, NewznabStandardCategory.TVSport, "|- Документальные фильмы (футбол)"); caps.Categories.AddCategoryMapping(1697, NewznabStandardCategory.TVSport, "|- Мини-футбол/Пляжный футбол"); @@ -476,7 +481,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(2001, NewznabStandardCategory.TVSport, "|- Международные соревнования"); caps.Categories.AddCategoryMapping(2002, NewznabStandardCategory.TVSport, "|- NBA / NCAA (до 2000 г.)"); caps.Categories.AddCategoryMapping(283, NewznabStandardCategory.TVSport, "|- NBA / NCAA (2000-2010 гг.)"); - caps.Categories.AddCategoryMapping(1997, NewznabStandardCategory.TVSport, "|- NBA / NCAA (2010-2023 гг.)"); + caps.Categories.AddCategoryMapping(1997, NewznabStandardCategory.TVSport, "|- NBA / NCAA (2010-2024 гг.)"); caps.Categories.AddCategoryMapping(2003, NewznabStandardCategory.TVSport, "|- Европейский клубный баскетбол"); caps.Categories.AddCategoryMapping(2009, NewznabStandardCategory.TVSport, "🏒 Хоккей"); caps.Categories.AddCategoryMapping(2010, NewznabStandardCategory.TVSport, "|- Хоккей с мячом / Бенди"); @@ -678,9 +683,9 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(2441, NewznabStandardCategory.BooksEBook, "|- Кулинария. Цветоводство. Домоводство"); caps.Categories.AddCategoryMapping(2442, NewznabStandardCategory.BooksEBook, "|- Культура. Искусство. История"); caps.Categories.AddCategoryMapping(2125, NewznabStandardCategory.Books, "Медицина и здоровье"); - caps.Categories.AddCategoryMapping(2133, NewznabStandardCategory.Books, "|- Клиническая медицина до 1980 г."); - caps.Categories.AddCategoryMapping(2130, NewznabStandardCategory.Books, "|- Клиническая медицина с 1980 по 2000 г."); - caps.Categories.AddCategoryMapping(2313, NewznabStandardCategory.Books, "|- Клиническая медицина после 2000 г."); + caps.Categories.AddCategoryMapping(2133, NewznabStandardCategory.Books, "|- Клиническая медицина до 1980 год"); + caps.Categories.AddCategoryMapping(2130, NewznabStandardCategory.Books, "|- Клиническая медицина с 1980 по 2000 год"); + caps.Categories.AddCategoryMapping(2313, NewznabStandardCategory.Books, "|- Клиническая медицина после 2000 год"); caps.Categories.AddCategoryMapping(2528, NewznabStandardCategory.Books, "|- Научная медицинская периодика (газеты и журналы)"); caps.Categories.AddCategoryMapping(2129, NewznabStandardCategory.Books, "|- Медико-биологические науки"); caps.Categories.AddCategoryMapping(2141, NewznabStandardCategory.Books, "|- Фармация и фармакология"); @@ -771,6 +776,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1592, NewznabStandardCategory.BooksOther, "|- Ушу"); caps.Categories.AddCategoryMapping(1595, NewznabStandardCategory.BooksOther, "|- Разное"); caps.Categories.AddCategoryMapping(1556, NewznabStandardCategory.BooksTechnical, "Компьютерные видеоуроки и обучающие интерактивные DVD"); + caps.Categories.AddCategoryMapping(2539, NewznabStandardCategory.BooksTechnical, "|- Machine/Deep Learning, Neural Networks"); caps.Categories.AddCategoryMapping(1560, NewznabStandardCategory.BooksTechnical, "|- Компьютерные сети и безопасность"); caps.Categories.AddCategoryMapping(1991, NewznabStandardCategory.BooksTechnical, "|- Devops"); caps.Categories.AddCategoryMapping(1561, NewznabStandardCategory.BooksTechnical, "|- ОС и серверные программы Microsoft"); @@ -928,9 +934,13 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1224, NewznabStandardCategory.AudioLossless, "|- Авторская песня (lossless)"); caps.Categories.AddCategoryMapping(1225, NewznabStandardCategory.AudioMP3, "|- Авторская песня (lossy)"); caps.Categories.AddCategoryMapping(1226, NewznabStandardCategory.Audio, "|- Менестрели и ролевики (lossy и lossless)"); + caps.Categories.AddCategoryMapping(782, NewznabStandardCategory.Audio, "Лейбл- и сцен-паки. Неофициальные сборники и ремастеринги. AI-музыка"); + caps.Categories.AddCategoryMapping(577, NewznabStandardCategory.Audio, "|- AI-Music - музыка ИИ, нейросетей (lossy и lossless)"); caps.Categories.AddCategoryMapping(1842, NewznabStandardCategory.AudioLossless, "Label Packs (lossless)"); caps.Categories.AddCategoryMapping(1648, NewznabStandardCategory.AudioMP3, "Label packs, Scene packs (lossy)"); - caps.Categories.AddCategoryMapping(2495, NewznabStandardCategory.Audio, "Отечественная поп-музыка"); + caps.Categories.AddCategoryMapping(134, NewznabStandardCategory.AudioLossless, "|- Неофициальные сборники и ремастеринги (lossless)"); + caps.Categories.AddCategoryMapping(965, NewznabStandardCategory.AudioMP3, "|- Неофициальные сборники (lossy)"); + caps.Categories.AddCategoryMapping(2495, NewznabStandardCategory.AudioMP3, "Отечественная поп-музыка "); caps.Categories.AddCategoryMapping(424, NewznabStandardCategory.AudioMP3, "|- Популярная музыка России и стран бывшего СССР (lossy)"); caps.Categories.AddCategoryMapping(1361, NewznabStandardCategory.AudioMP3, "|- Популярная музыка России и стран бывшего СССР (сборники) (lossy)"); caps.Categories.AddCategoryMapping(425, NewznabStandardCategory.AudioLossless, "|- Популярная музыка России и стран бывшего СССР (lossless)"); @@ -1172,49 +1182,50 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1912, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (Видео)"); caps.Categories.AddCategoryMapping(1189, NewznabStandardCategory.AudioVideo, "|- Документальные фильмы о музыке и музыкантах (Видео)"); caps.Categories.AddCategoryMapping(2403, NewznabStandardCategory.AudioVideo, "Музыкальное DVD видео"); - caps.Categories.AddCategoryMapping(984, NewznabStandardCategory.AudioVideo, "|- Классическая и современная академическая музыка (DVD Video)"); - caps.Categories.AddCategoryMapping(983, NewznabStandardCategory.AudioVideo, "|- Опера, Оперетта и Мюзикл (DVD видео)"); - caps.Categories.AddCategoryMapping(2352, NewznabStandardCategory.AudioVideo, "|- Балет и современная хореография (DVD Video)"); - caps.Categories.AddCategoryMapping(2384, NewznabStandardCategory.AudioVideo, "|- Классика в современной обработке, Classical Crossover (DVD Video)"); - caps.Categories.AddCategoryMapping(1142, NewznabStandardCategory.AudioVideo, "|- Фольклор, Народная и Этническая музыка и Flamenco (DVD Video)"); - caps.Categories.AddCategoryMapping(1107, NewznabStandardCategory.AudioVideo, "|- New Age, Relax, Meditative, Рэп, Хип-Хоп, R'n'B, Reggae, Ska, Dub (DVD Video)"); - caps.Categories.AddCategoryMapping(1228, NewznabStandardCategory.AudioVideo, "|- Зарубежный и Отечественный Шансон, Авторская и Военная песня (DVD Video)"); - caps.Categories.AddCategoryMapping(988, NewznabStandardCategory.AudioVideo, "|- Музыка других жанров, Советская эстрада, ретро, романсы (DVD Video)"); - caps.Categories.AddCategoryMapping(1122, NewznabStandardCategory.AudioVideo, "|- Отечественная поп-музыка (DVD Video)"); - caps.Categories.AddCategoryMapping(986, NewznabStandardCategory.AudioVideo, "|- Зарубежная Поп-музыка, Eurodance, Disco (DVD Video)"); - caps.Categories.AddCategoryMapping(2379, NewznabStandardCategory.AudioVideo, "|- Восточноазиатская поп-музыка (DVD Video)"); - caps.Categories.AddCategoryMapping(2088, NewznabStandardCategory.AudioVideo, "|- Разножанровые сборные концерты и сборники видеоклипов (DVD Video)"); + caps.Categories.AddCategoryMapping(984, NewznabStandardCategory.AudioVideo, "|- Классическая и современная академическая музыка (DVD Видео)"); + caps.Categories.AddCategoryMapping(983, NewznabStandardCategory.AudioVideo, "|- Опера, Оперетта и Мюзикл (DVD Видео)"); + caps.Categories.AddCategoryMapping(2352, NewznabStandardCategory.AudioVideo, "|- Балет и современная хореография (DVD Видео)"); + caps.Categories.AddCategoryMapping(2384, NewznabStandardCategory.AudioVideo, "|- Классика в современной обработке, Classical Crossover (DVD Видео)"); + caps.Categories.AddCategoryMapping(1142, NewznabStandardCategory.AudioVideo, "|- Фольклор, Народная и Этническая музыка и Flamenco (DVD Видео)"); + caps.Categories.AddCategoryMapping(1107, NewznabStandardCategory.AudioVideo, "|- New Age, Relax, Meditative, Рэп, Хип-Хоп, R'n'B, Reggae, Ska, Dub (DVD Видео)"); + caps.Categories.AddCategoryMapping(1228, NewznabStandardCategory.AudioVideo, "|- Зарубежный и Отечественный Шансон, Авторская и Военная песня (DVD Видео)"); + caps.Categories.AddCategoryMapping(988, NewznabStandardCategory.AudioVideo, "|- Музыка других жанров, Советская эстрада, ретро, романсы (DVD Видео)"); + caps.Categories.AddCategoryMapping(1122, NewznabStandardCategory.AudioVideo, "|- Отечественная поп-музыка (DVD Видео)"); + caps.Categories.AddCategoryMapping(986, NewznabStandardCategory.AudioVideo, "|- Зарубежная Поп-музыка, Eurodance, Disco (DVD Видео)"); + caps.Categories.AddCategoryMapping(2379, NewznabStandardCategory.AudioVideo, "|- Восточноазиатская поп-музыка (DVD Видео)"); + caps.Categories.AddCategoryMapping(2088, NewznabStandardCategory.AudioVideo, "|- Разножанровые сборные концерты и сборники видеоклипов (DVD Видео)"); caps.Categories.AddCategoryMapping(2304, NewznabStandardCategory.AudioVideo, "|- Джаз и Блюз (DVD Видео)"); - caps.Categories.AddCategoryMapping(1783, NewznabStandardCategory.AudioVideo, "|- Зарубежный Rock (DVD Video)"); - caps.Categories.AddCategoryMapping(1788, NewznabStandardCategory.AudioVideo, "|- Зарубежный Metal (DVD Video)"); - caps.Categories.AddCategoryMapping(1790, NewznabStandardCategory.AudioVideo, "|- Зарубежный Alternative, Punk, Independent (DVD Video)"); - caps.Categories.AddCategoryMapping(1792, NewznabStandardCategory.AudioVideo, "|- Отечественный Рок, Метал, Панк, Альтернатива (DVD Video)"); - caps.Categories.AddCategoryMapping(1886, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (DVD Video)"); - caps.Categories.AddCategoryMapping(2509, NewznabStandardCategory.AudioVideo, "|- Документальные фильмы о музыке и музыкантах (DVD Video)"); + caps.Categories.AddCategoryMapping(1783, NewznabStandardCategory.AudioVideo, "|- Зарубежный Rock (DVD Видео)"); + caps.Categories.AddCategoryMapping(1788, NewznabStandardCategory.AudioVideo, "|- Зарубежный Metal (DVD Видео)"); + caps.Categories.AddCategoryMapping(1790, NewznabStandardCategory.AudioVideo, "|- Зарубежный Alternative, Punk, Independent (DVD Видео)"); + caps.Categories.AddCategoryMapping(1792, NewznabStandardCategory.AudioVideo, "|- Отечественный Рок, Метал, Панк, Альтернатива (DVD Видео)"); + caps.Categories.AddCategoryMapping(1886, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (DVD Видео)"); + caps.Categories.AddCategoryMapping(2509, NewznabStandardCategory.AudioVideo, "|- Документальные фильмы о музыке и музыкантах (DVD Видео)"); caps.Categories.AddCategoryMapping(2507, NewznabStandardCategory.AudioVideo, "Неофициальные DVD видео"); - caps.Categories.AddCategoryMapping(2263, NewznabStandardCategory.AudioVideo, "|- Классическая музыка, Опера, Балет, Мюзикл (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(2511, NewznabStandardCategory.AudioVideo, "|- Шансон, Авторская песня, Сборные концерты, МДЖ (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(2264, NewznabStandardCategory.AudioVideo, "|- Зарубежная и Отечественная Поп-музыка (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(2262, NewznabStandardCategory.AudioVideo, "|- Джаз и Блюз (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(2261, NewznabStandardCategory.AudioVideo, "|- Зарубежная и Отечественная Рок-музыка (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(1887, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (Неофициальные DVD Video)"); - caps.Categories.AddCategoryMapping(2531, NewznabStandardCategory.AudioVideo, "|- Прочие жанры (Неофициальные DVD видео)"); + caps.Categories.AddCategoryMapping(2263, NewznabStandardCategory.AudioVideo, "|- Классическая музыка, Опера, Балет, Мюзикл (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(2511, NewznabStandardCategory.AudioVideo, "|- Шансон, Авторская песня, Сборные концерты, МДЖ (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(2264, NewznabStandardCategory.AudioVideo, "|- Зарубежная и Отечественная Поп-музыка (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(2262, NewznabStandardCategory.AudioVideo, "|- Джаз и Блюз (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(2261, NewznabStandardCategory.AudioVideo, "|- Зарубежная и Отечественная Рок-музыка (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(1887, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (Неофициальные DVD Видео)"); + caps.Categories.AddCategoryMapping(2531, NewznabStandardCategory.AudioVideo, "|- Прочие жанры (Неофициальные DVD Видео)"); caps.Categories.AddCategoryMapping(2400, NewznabStandardCategory.AudioVideo, "Музыкальное HD видео"); - caps.Categories.AddCategoryMapping(1812, NewznabStandardCategory.AudioVideo, "|- Классическая и современная академическая музыка (HD Video)"); + caps.Categories.AddCategoryMapping(1812, NewznabStandardCategory.AudioVideo, "|- Классическая и современная академическая музыка (HD Видео)"); caps.Categories.AddCategoryMapping(655, NewznabStandardCategory.AudioVideo, "|- Опера, Оперетта и Мюзикл (HD Видео)"); - caps.Categories.AddCategoryMapping(1777, NewznabStandardCategory.AudioVideo, "|- Балет и современная хореография (HD Video)"); + caps.Categories.AddCategoryMapping(1777, NewznabStandardCategory.AudioVideo, "|- Балет и современная хореография (HD Видео)"); caps.Categories.AddCategoryMapping(2530, NewznabStandardCategory.AudioVideo, "|- Фольклор, Народная, Этническая музыка и Flamenco (HD Видео)"); caps.Categories.AddCategoryMapping(2529, NewznabStandardCategory.AudioVideo, "|- New Age, Relax, Meditative, Рэп, Хип-Хоп, R'n'B, Reggae, Ska, Dub (HD Видео)"); caps.Categories.AddCategoryMapping(1781, NewznabStandardCategory.AudioVideo, "|- Музыка других жанров, Разножанровые сборные концерты (HD видео)"); - caps.Categories.AddCategoryMapping(2508, NewznabStandardCategory.AudioVideo, "|- Зарубежная поп-музыка (HD Video)"); + caps.Categories.AddCategoryMapping(2508, NewznabStandardCategory.AudioVideo, "|- Зарубежная поп-музыка (HD Видео)"); caps.Categories.AddCategoryMapping(2426, NewznabStandardCategory.AudioVideo, "|- Отечественная поп-музыка (HD видео)"); - caps.Categories.AddCategoryMapping(2351, NewznabStandardCategory.AudioVideo, "|- Восточноазиатская Поп-музыка (HD Video)"); - caps.Categories.AddCategoryMapping(2306, NewznabStandardCategory.AudioVideo, "|- Джаз и Блюз (HD Video)"); - caps.Categories.AddCategoryMapping(1795, NewznabStandardCategory.AudioVideo, "|- Зарубежный рок (HD Video)"); + caps.Categories.AddCategoryMapping(2351, NewznabStandardCategory.AudioVideo, "|- Восточноазиатская Поп-музыка (HD Видео)"); + caps.Categories.AddCategoryMapping(2306, NewznabStandardCategory.AudioVideo, "|- Джаз и Блюз (HD Видео)"); + caps.Categories.AddCategoryMapping(1795, NewznabStandardCategory.AudioVideo, "|- Зарубежный рок (HD Видео)"); caps.Categories.AddCategoryMapping(2271, NewznabStandardCategory.AudioVideo, "|- Отечественный рок (HD видео)"); - caps.Categories.AddCategoryMapping(1913, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (HD Video)"); + caps.Categories.AddCategoryMapping(1913, NewznabStandardCategory.AudioVideo, "|- Электронная музыка (HD Видео)"); caps.Categories.AddCategoryMapping(1784, NewznabStandardCategory.AudioVideo, "|- UHD музыкальное видео"); - caps.Categories.AddCategoryMapping(1892, NewznabStandardCategory.AudioVideo, "|- Документальные фильмы о музыке и музыкантах (HD Video)"); + caps.Categories.AddCategoryMapping(1892, NewznabStandardCategory.AudioVideo, "|- Документальные фильмы о музыке и музыкантах (HD Видео)"); + caps.Categories.AddCategoryMapping(2266, NewznabStandardCategory.AudioVideo, "|- Официальные апскейлы (Blu-ray, HDTV, WEB-DL)"); caps.Categories.AddCategoryMapping(518, NewznabStandardCategory.AudioVideo, "Некондиционное музыкальное видео (Видео, DVD видео, HD видео)"); caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.PCGames, "Игры для Windows"); caps.Categories.AddCategoryMapping(635, NewznabStandardCategory.PCGames, "|- Горячие Новинки"); @@ -1229,11 +1240,16 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(128, NewznabStandardCategory.PCGames, "|- Для самых маленьких"); caps.Categories.AddCategoryMapping(2204, NewznabStandardCategory.PCGames, "|- Логические игры"); caps.Categories.AddCategoryMapping(278, NewznabStandardCategory.PCGames, "|- Шахматы"); - caps.Categories.AddCategoryMapping(2118, NewznabStandardCategory.PCGames, "|- Многопользовательские игры"); caps.Categories.AddCategoryMapping(52, NewznabStandardCategory.PCGames, "|- Ролевые игры"); caps.Categories.AddCategoryMapping(54, NewznabStandardCategory.PCGames, "|- Симуляторы"); caps.Categories.AddCategoryMapping(51, NewznabStandardCategory.PCGames, "|- Стратегии в реальном времени"); caps.Categories.AddCategoryMapping(2226, NewznabStandardCategory.PCGames, "|- Пошаговые стратегии"); + caps.Categories.AddCategoryMapping(2118, NewznabStandardCategory.PCGames, "|- Антологии и сборники игр"); + caps.Categories.AddCategoryMapping(1310, NewznabStandardCategory.PCGames, "|- Старые игры (Экшены)"); + caps.Categories.AddCategoryMapping(2410, NewznabStandardCategory.PCGames, "|- Старые игры (Ролевые игры)"); + caps.Categories.AddCategoryMapping(2205, NewznabStandardCategory.PCGames, "|- Старые игры (Стратегии)"); + caps.Categories.AddCategoryMapping(2225, NewznabStandardCategory.PCGames, "|- Старые игры (Приключения и квесты)"); + caps.Categories.AddCategoryMapping(2206, NewznabStandardCategory.PCGames, "|- Старые игры (Симуляторы)"); caps.Categories.AddCategoryMapping(2228, NewznabStandardCategory.PCGames, "|- IBM-PC-несовместимые компьютеры"); caps.Categories.AddCategoryMapping(139, NewznabStandardCategory.PCGames, "Прочее для Windows-игр"); caps.Categories.AddCategoryMapping(2478, NewznabStandardCategory.PCGames, "|- Официальные патчи, моды, плагины, дополнения"); @@ -1255,8 +1271,8 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(908, NewznabStandardCategory.Console, "|- PS"); caps.Categories.AddCategoryMapping(357, NewznabStandardCategory.ConsoleOther, "|- PS2"); caps.Categories.AddCategoryMapping(886, NewznabStandardCategory.ConsolePS3, "|- PS3"); - caps.Categories.AddCategoryMapping(546, NewznabStandardCategory.Console, "|- Игры PS1, PS2 и PSP для PS3"); caps.Categories.AddCategoryMapping(973, NewznabStandardCategory.ConsolePS4, "|- PS4"); + caps.Categories.AddCategoryMapping(546, NewznabStandardCategory.ConsoleOther, "|- PS5"); caps.Categories.AddCategoryMapping(1352, NewznabStandardCategory.ConsolePSP, "|- PSP"); caps.Categories.AddCategoryMapping(1116, NewznabStandardCategory.ConsolePSP, "|- Игры PS1 для PSP"); caps.Categories.AddCategoryMapping(595, NewznabStandardCategory.ConsolePSVita, "|- PS Vita"); @@ -1279,7 +1295,6 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(650, NewznabStandardCategory.PCMobileOther, "Игры для мобильных устройств"); caps.Categories.AddCategoryMapping(2149, NewznabStandardCategory.PCMobileAndroid, "|- Игры для Android"); caps.Categories.AddCategoryMapping(2420, NewznabStandardCategory.ConsoleOther, "|- Игры для Oculus Quest"); - caps.Categories.AddCategoryMapping(1001, NewznabStandardCategory.PC, "|- Игры для Java"); caps.Categories.AddCategoryMapping(1004, NewznabStandardCategory.PCMobileOther, "|- Игры для Symbian"); caps.Categories.AddCategoryMapping(1002, NewznabStandardCategory.PCMobileOther, "|- Игры для Windows Mobile"); caps.Categories.AddCategoryMapping(240, NewznabStandardCategory.OtherMisc, "Игровое видео"); @@ -1295,7 +1310,6 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1379, NewznabStandardCategory.PC, "|- Операционные системы (Linux, Unix)"); caps.Categories.AddCategoryMapping(1381, NewznabStandardCategory.PC, "|- Программное обеспечение (Linux, Unix)"); caps.Categories.AddCategoryMapping(1473, NewznabStandardCategory.PC, "|- Другие ОС и ПО под них"); - caps.Categories.AddCategoryMapping(1195, NewznabStandardCategory.PC, "Тестовые диски для настройки аудио/видео аппаратуры"); caps.Categories.AddCategoryMapping(1013, NewznabStandardCategory.PC, "Системные программы"); caps.Categories.AddCategoryMapping(1028, NewznabStandardCategory.PC, "|- Работа с жёстким диском"); caps.Categories.AddCategoryMapping(1029, NewznabStandardCategory.PC, "|- Резервное копирование"); @@ -1306,15 +1320,12 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1034, NewznabStandardCategory.PC, "|- Информация и диагностика"); caps.Categories.AddCategoryMapping(1066, NewznabStandardCategory.PC, "|- Программы для интернет и сетей"); caps.Categories.AddCategoryMapping(1035, NewznabStandardCategory.PC, "|- ПО для защиты компьютера (Антивирусное ПО, Фаерволлы)"); - caps.Categories.AddCategoryMapping(1038, NewznabStandardCategory.PC, "|- Анти-шпионы и анти-трояны"); - caps.Categories.AddCategoryMapping(1039, NewznabStandardCategory.PC, "|- Программы для защиты информации"); caps.Categories.AddCategoryMapping(1536, NewznabStandardCategory.PC, "|- Драйверы и прошивки"); caps.Categories.AddCategoryMapping(1051, NewznabStandardCategory.PC, "|- Оригинальные диски к компьютерам и комплектующим"); caps.Categories.AddCategoryMapping(1040, NewznabStandardCategory.PC, "|- Серверное ПО для Windows"); caps.Categories.AddCategoryMapping(1041, NewznabStandardCategory.PC, "|- Изменение интерфейса ОС Windows"); caps.Categories.AddCategoryMapping(1636, NewznabStandardCategory.PC, "|- Скринсейверы"); caps.Categories.AddCategoryMapping(1042, NewznabStandardCategory.PC, "|- Разное (Системные программы под Windows)"); - caps.Categories.AddCategoryMapping(1059, NewznabStandardCategory.PC, "|- Архив (Разрегистрированные раздачи)"); caps.Categories.AddCategoryMapping(1014, NewznabStandardCategory.PC, "Системы для бизнеса, офиса, научной и проектной работы"); caps.Categories.AddCategoryMapping(2134, NewznabStandardCategory.PC, "|- Медицина - интерактивный софт"); caps.Categories.AddCategoryMapping(1060, NewznabStandardCategory.PC, "|- Всё для дома: кройка, шитьё, кулинария"); @@ -1340,6 +1351,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(1018, NewznabStandardCategory.PC, "|- Шаблоны для сайтов и CMS"); caps.Categories.AddCategoryMapping(1058, NewznabStandardCategory.PC, "|- Разное (Веб-разработка и программирование)"); caps.Categories.AddCategoryMapping(1016, NewznabStandardCategory.PC, "Программы для работы с мультимедиа и 3D"); + caps.Categories.AddCategoryMapping(1195, NewznabStandardCategory.PC, "|- Тестовые диски для настройки аудио/видео аппаратуры"); caps.Categories.AddCategoryMapping(1079, NewznabStandardCategory.PC, "|- Программные комплекты"); caps.Categories.AddCategoryMapping(1080, NewznabStandardCategory.PC, "|- Плагины для программ компании Adobe"); caps.Categories.AddCategoryMapping(1081, NewznabStandardCategory.PC, "|- Графические редакторы"); @@ -1442,8 +1454,6 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping(630, NewznabStandardCategory.OtherMisc, "|- Обои"); caps.Categories.AddCategoryMapping(1664, NewznabStandardCategory.OtherMisc, "|- Фото знаменитостей"); caps.Categories.AddCategoryMapping(148, NewznabStandardCategory.Audio, "|- Аудио"); - caps.Categories.AddCategoryMapping(965, NewznabStandardCategory.AudioMP3, "|- Музыка (lossy)"); - caps.Categories.AddCategoryMapping(134, NewznabStandardCategory.AudioLossless, "|- Музыка (lossless)"); caps.Categories.AddCategoryMapping(807, NewznabStandardCategory.TVOther, "|- Видео"); caps.Categories.AddCategoryMapping(147, NewznabStandardCategory.Books, "|- Публикации и учебные материалы (тексты)"); caps.Categories.AddCategoryMapping(847, NewznabStandardCategory.MoviesOther, "|- Трейлеры и дополнительные материалы к фильмам"); @@ -1467,27 +1477,27 @@ namespace NzbDrone.Core.Indexers.Definitions public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { - return GetPageableRequests(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + return GetPageableRequests(searchCriteria.SearchTerm, searchCriteria.Categories); } public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) { - return GetPageableRequests(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + return GetPageableRequests(searchCriteria.SearchTerm, searchCriteria.Categories); } public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) { - return GetPageableRequests(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.Season ?? 0); + return GetPageableRequests(searchCriteria.SearchTerm, searchCriteria.Categories, searchCriteria.Season ?? 0); } public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) { - return GetPageableRequests(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + return GetPageableRequests(searchCriteria.SearchTerm, searchCriteria.Categories); } public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) { - return GetPageableRequests(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + return GetPageableRequests(searchCriteria.SearchTerm, searchCriteria.Categories); } private IndexerPageableRequestChain GetPageableRequests(string searchTerm, int[] categories, int season = 0) @@ -1525,8 +1535,10 @@ namespace NzbDrone.Core.Indexers.Definitions } else { - // use the normal search + // replace any space, special char, etc. with % (wildcard) + searchString = new Regex("[^a-zA-Zа-яА-ЯёЁ0-9]+").Replace(searchString, "%"); searchString = searchString.Replace("-", " "); + if (season != 0) { searchString += " Сезон: " + season; @@ -1740,7 +1752,7 @@ namespace NzbDrone.Core.Indexers.Definitions title = Regex.Replace(title, @"(\([\p{IsCyrillic}\W]+)\s/\s(.+?)\)", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase); // Remove VO, MVO and DVO from titles - var vo = new Regex(@".VO\s\(.+?\)"); + var vo = new Regex(@"((?:\dx\s)?(?:[A-Z])?VO\s\(.+?\))"); title = vo.Replace(title, string.Empty); // Remove R5 and (R5) from release names @@ -1748,7 +1760,7 @@ namespace NzbDrone.Core.Indexers.Definitions title = r5.Replace(title, "$1"); // Remove Sub languages from release names - title = Regex.Replace(title, @"(\bSub\b.*$|\b[\+]*Sub[\+]*\b)", string.Empty); + title = Regex.Replace(title, @"(\bSub\b[^+]*\b|\b[\+]*Sub[\+]*\b)", string.Empty); } // language fix: all rutracker releases contains russian track diff --git a/src/NzbDrone.Core/Indexers/Definitions/SceneHD.cs b/src/NzbDrone.Core/Indexers/Definitions/SceneHD.cs index e554c4d33..505af4f0c 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SceneHD.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SceneHD.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new SceneHDRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; + return new SceneHDRequestGenerator(Settings, Capabilities); } public override IParseIndexerResponse GetParser() @@ -88,38 +88,41 @@ namespace NzbDrone.Core.Indexers.Definitions public class SceneHDRequestGenerator : IIndexerRequestGenerator { - public SceneHDSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - public string BaseUrl { get; set; } + private readonly SceneHDSettings _settings; + private readonly IndexerCapabilities _capabilities; + + public SceneHDRequestGenerator(SceneHDSettings settings, IndexerCapabilities capabilities) + { + _settings = settings; + _capabilities = capabilities; + } private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null) { var search = new[] { imdbId, term }; - var qc = new NameValueCollection + var parameters = new NameValueCollection { { "api", "" }, - { "passkey", Settings.Passkey }, + { "passkey", _settings.Passkey }, { "search", string.Join(" ", search.Where(s => s.IsNotNullOrWhiteSpace())) } }; - foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories)) + if (categories?.Length > 0) { - qc.Add("categories[" + cat + "]", "1"); + parameters.Add("cat", _capabilities.Categories.MapTorznabCapsToTrackers(categories).Distinct().Join(",")); } - var searchUrl = string.Format("{0}/browse.php?{1}", Settings.BaseUrl.TrimEnd('/'), qc.GetQueryString()); + var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/browse.php?{parameters.GetQueryString()}"; - var request = new IndexerRequest(searchUrl, HttpAccept.Json); - - yield return request; + yield return new IndexerRequest(searchUrl, HttpAccept.Json); } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId)); return pageableRequests; } @@ -128,7 +131,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); return pageableRequests; } @@ -137,7 +140,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories, searchCriteria.FullImdbId)); return pageableRequests; } @@ -146,7 +149,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); return pageableRequests; } @@ -155,7 +158,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); return pageableRequests; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/SecretCinema.cs b/src/NzbDrone.Core/Indexers/Definitions/SecretCinema.cs index 53b688450..e8d8b1584 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SecretCinema.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SecretCinema.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Definitions.Gazelle; using NzbDrone.Core.Indexers.Exceptions; @@ -19,7 +20,7 @@ public class SecretCinema : GazelleBase<GazelleSettings> { public override string Name => "Secret Cinema"; public override string[] IndexerUrls => new[] { "https://secret-cinema.pw/" }; - public override string Description => "A tracker for rare movies."; + public override string Description => "Secret Cinema is a Private ratioless site for rare MOVIES."; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override IndexerCapabilities Capabilities => SetCapabilities(); @@ -78,7 +79,9 @@ public class SecretCinemaParser : IParseIndexerResponse // Remove cookie cache CookiesUpdater(null, null); - throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request"); + STJson.TryDeserialize<GazelleErrorResponse>(indexerResponse.Content, out var errorResponse); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from indexer request: {errorResponse?.Error ?? "Check the logs for more information."}"); } if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) @@ -104,6 +107,7 @@ public class SecretCinemaParser : IParseIndexerResponse foreach (var torrent in result.Torrents) { var id = torrent.TorrentId; + var infoUrl = GetInfoUrl(result.GroupId, id); // in SC movies, artist=director and GroupName=title var artist = WebUtility.HtmlDecode(result.Artist); @@ -112,15 +116,15 @@ public class SecretCinemaParser : IParseIndexerResponse var release = new TorrentInfo { - Guid = $"SecretCinema-{id}", + Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id), Title = title, Container = torrent.Encoding, Files = torrent.FileCount, Grabs = torrent.Snatches, Codec = torrent.Format, Size = long.Parse(torrent.Size), - DownloadUrl = GetDownloadUrl(id), - InfoUrl = GetInfoUrl(result.GroupId, id), Seeders = int.Parse(torrent.Seeders), Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), PublishDate = new DateTimeOffset(time, TimeSpan.FromHours(2)).UtcDateTime, @@ -143,7 +147,12 @@ public class SecretCinemaParser : IParseIndexerResponse { // Remove director from title // SC API returns no more useful information than this - release.Title = $"{title} ({result.GroupYear}) {torrent.Media}"; + release.Title = $"{title} ({result.GroupYear}) {torrent.Media}".Trim(); + + if (torrent.RemasterTitle.IsNotNullOrWhiteSpace()) + { + release.Title += $" [{WebUtility.HtmlDecode(torrent.RemasterTitle).Trim()}]"; + } // Replace media formats with standards release.Title = Regex.Replace(release.Title, @"\bBDMV\b", "COMPLETE BLURAY", RegexOptions.IgnoreCase); @@ -168,15 +177,16 @@ public class SecretCinemaParser : IParseIndexerResponse else { var id = result.TorrentId; + var infoUrl = GetInfoUrl(result.GroupId, id); var groupName = WebUtility.HtmlDecode(result.GroupName); var release = new TorrentInfo { - Guid = $"SecretCinema-{id}", + Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(id), Title = groupName, Size = long.Parse(result.Size), - DownloadUrl = GetDownloadUrl(id), - InfoUrl = GetInfoUrl(result.GroupId, id), Seeders = int.Parse(result.Seeders), Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders), Files = result.FileCount, @@ -209,7 +219,7 @@ public class SecretCinemaParser : IParseIndexerResponse private bool IsAnyMovieCategory(ICollection<IndexerCategory> category) { - return category.Contains(NewznabStandardCategory.Movies) || NewznabStandardCategory.Movies.SubCategories.Any(subCat => category.Contains(subCat)); + return category.Contains(NewznabStandardCategory.Movies) || NewznabStandardCategory.Movies.SubCategories.Any(category.Contains); } private string GetDownloadUrl(int torrentId) @@ -219,7 +229,7 @@ public class SecretCinemaParser : IParseIndexerResponse .AddQueryParam("action", "download") .AddQueryParam("id", torrentId); - if (_settings.UseFreeleechToken) + if (_settings.UseFreeleechToken is (int)GazelleFreeleechTokenAction.Preferred or (int)GazelleFreeleechTokenAction.Required) { url = url.AddQueryParam("useToken", "1"); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs b/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs index 61a5f0182..43793770f 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs @@ -19,6 +19,7 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Definitions; @@ -50,7 +51,7 @@ public class Shazbat : TorrentIndexerBase<ShazbatSettings> public override IParseIndexerResponse GetParser() { - return new ShazbatParser(Settings, RateLimit, _httpClient, _logger); + return new ShazbatParser(Definition, Settings, RateLimit, _httpClient, _logger); } protected override async Task DoLogin() @@ -101,7 +102,7 @@ public class Shazbat : TorrentIndexerBase<ShazbatSettings> { TvSearchParams = new List<TvSearchParam> { - TvSearchParam.Q + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep }, Flags = new List<IndexerFlag> { @@ -202,6 +203,7 @@ public class ShazbatRequestGenerator : IIndexerRequestGenerator public class ShazbatParser : IParseIndexerResponse { + private readonly ProviderDefinition _definition; private readonly ShazbatSettings _settings; private readonly TimeSpan _rateLimit; private readonly IIndexerHttpClient _httpClient; @@ -210,8 +212,9 @@ public class ShazbatParser : IParseIndexerResponse private readonly Regex _torrentInfoRegex = new (@"\((?<size>\d+)\):(?<seeders>\d+) \/ :(?<leechers>\d+)$", RegexOptions.Compiled); private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" }; - public ShazbatParser(ShazbatSettings settings, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger) + public ShazbatParser(ProviderDefinition definition, ShazbatSettings settings, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger) { + _definition = definition; _settings = settings; _rateLimit = rateLimit; _httpClient = httpClient; @@ -272,7 +275,7 @@ public class ShazbatParser : IParseIndexerResponse _logger.Debug("Downloading Feed " + showRequest.ToString()); var releaseRequest = new IndexerRequest(showRequest); - var releaseResponse = new IndexerResponse(releaseRequest, _httpClient.Execute(releaseRequest.HttpRequest)); + var releaseResponse = new IndexerResponse(releaseRequest, _httpClient.ExecuteProxied(releaseRequest.HttpRequest, _definition)); if (releaseResponse.HttpResponse.Content.ContainsIgnoreCase("sign in now")) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs b/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs index e817a41a0..fc565775d 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Shizaproject.cs @@ -17,6 +17,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Definitions { + [Obsolete("Site unusable due to lack of new releases")] public class Shizaproject : TorrentIndexerBase<NoAuthTorrentBaseSettings> { public override string Name => "ShizaProject"; diff --git a/src/NzbDrone.Core/Indexers/Definitions/SpeedApp.cs b/src/NzbDrone.Core/Indexers/Definitions/SpeedApp.cs index d89837432..c1dfc58ba 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SpeedApp.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SpeedApp.cs @@ -1,10 +1,7 @@ using System.Collections.Generic; -using System.Linq; using NLog; using NzbDrone.Core.Configuration; -using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Definitions { @@ -22,13 +19,6 @@ namespace NzbDrone.Core.Indexers.Definitions { } - protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria) - { - var cleanReleases = base.CleanupReleases(releases, searchCriteria); - - return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); - } - protected override IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities diff --git a/src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs b/src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs index e1e878160..244a9c286 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Mime; using System.Text; using System.Text.RegularExpressions; @@ -51,6 +50,13 @@ namespace NzbDrone.Core.Indexers.Definitions return new SpeedAppParser(Settings, Capabilities.Categories, MinimumSeedTime); } + protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria) + { + var cleanReleases = base.CleanupReleases(releases, searchCriteria); + + return FilterReleasesByQuery(cleanReleases, searchCriteria).ToList(); + } + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) { return Settings.ApiKey.IsNullOrWhiteSpace() || httpResponse.StatusCode == HttpStatusCode.Unauthorized; @@ -58,14 +64,13 @@ namespace NzbDrone.Core.Indexers.Definitions protected override async Task DoLogin() { - var requestBuilder = new HttpRequestBuilder(LoginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true, - Method = HttpMethod.Post, - }; - - var request = requestBuilder.Build(); + var request = new HttpRequestBuilder(LoginUrl) + { + LogResponseContent = true, + AllowAutoRedirect = true + } + .Post() + .Build(); var data = new SpeedAppAuthenticationRequest { @@ -257,7 +262,7 @@ namespace NzbDrone.Core.Indexers.Definitions return jsonResponse.Resource.Select(torrent => new TorrentInfo { - Guid = torrent.Id.ToString(), + Guid = torrent.Url, Title = CleanTitle(torrent.Name), Description = torrent.ShortDescription, Size = torrent.Size, @@ -311,7 +316,7 @@ namespace NzbDrone.Core.Indexers.Definitions [FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "API Key", Hidden = HiddenType.Hidden)] + [FieldDefinition(4, Label = "ApiKey", Hidden = HiddenType.Hidden)] public string ApiKey { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs b/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs index d395e0ba1..184a19f08 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Exceptions; @@ -43,12 +44,12 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new SubsPleaseRequestGenerator { Settings = Settings, Capabilities = Capabilities }; + return new SubsPleaseRequestGenerator(Settings); } public override IParseIndexerResponse GetParser() { - return new SubsPleaseParser(Settings, Capabilities.Categories); + return new SubsPleaseParser(Settings); } private IndexerCapabilities SetCapabilities() @@ -74,105 +75,110 @@ namespace NzbDrone.Core.Indexers.Definitions public class SubsPleaseRequestGenerator : IIndexerRequestGenerator { - public NoAuthTorrentBaseSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } + private static readonly Regex ResolutionRegex = new (@"\d{3,4}p", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private IEnumerable<IndexerRequest> GetSearchRequests(string term) + private readonly NoAuthTorrentBaseSettings _settings; + + public SubsPleaseRequestGenerator(NoAuthTorrentBaseSettings settings) { - var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api/?"; - - var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim(); - - // If the search terms contain a resolution, remove it from the query sent to the API - var resMatch = Regex.Match(searchTerm, "\\d{3,4}[p|P]"); - if (resMatch.Success) - { - searchTerm = searchTerm.Replace(resMatch.Value, string.Empty); - } - - var queryParameters = new NameValueCollection - { - { "f", "search" }, - { "tz", "UTC" }, - { "s", searchTerm } - }; - - var request = new IndexerRequest(searchUrl + queryParameters.GetQueryString(), HttpAccept.Json); - - yield return request; - } - - private IEnumerable<IndexerRequest> GetRssRequest() - { - var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api/?"; - - var queryParameters = new NameValueCollection - { - { "f", "latest" }, - { "tz", "UTC" } - }; - - var request = new IndexerRequest(searchUrl + queryParameters.GetQueryString(), HttpAccept.Json); - - yield return request; + _settings = settings; } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetSearchRequests(searchCriteria.SanitizedSearchTerm, searchCriteria)); + return pageableRequests; } public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - return pageableRequests; + return new IndexerPageableRequestChain(); } public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(searchCriteria.IsRssSearch - ? GetRssRequest() - : GetSearchRequests(searchCriteria.SanitizedTvSearchString)); + var searchTerm = searchCriteria.SanitizedSearchTerm.Trim(); + + // Only include season > 1 in searchTerm, format as S2 rather than S02 + if (searchCriteria.Season is > 1) + { + searchTerm += $" S{searchCriteria.Season}"; + } + + if (int.TryParse(searchCriteria.Episode, out var episode) && episode > 0) + { + searchTerm += $" {episode:00}"; + } + + pageableRequests.Add(GetSearchRequests(searchTerm, searchCriteria)); return pageableRequests; } public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - return pageableRequests; + return new IndexerPageableRequestChain(); } public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(searchCriteria.IsRssSearch - ? GetRssRequest() - : GetSearchRequests(searchCriteria.SanitizedSearchTerm)); + pageableRequests.Add(GetSearchRequests(searchCriteria.SanitizedSearchTerm, searchCriteria)); return pageableRequests; } + private IEnumerable<IndexerRequest> GetSearchRequests(string term, SearchCriteriaBase searchCriteria) + { + var queryParameters = new NameValueCollection + { + { "tz", "UTC" } + }; + + if (searchCriteria.IsRssSearch) + { + queryParameters.Set("f", "latest"); + } + else + { + var searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim(); + + // If the search terms contain a resolution, remove it from the query sent to the API + var resolutionMatch = ResolutionRegex.Match(searchTerm); + + if (resolutionMatch.Success) + { + searchTerm = searchTerm.Replace(resolutionMatch.Value, string.Empty).Trim(); + } + + queryParameters.Set("f", "search"); + queryParameters.Set("s", searchTerm); + } + + var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/api/?{queryParameters.GetQueryString()}"; + + yield return new IndexerRequest(searchUrl, HttpAccept.Json); + } + public Func<IDictionary<string, string>> GetCookies { get; set; } public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } } public class SubsPleaseParser : IParseIndexerResponse { - private readonly NoAuthTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; + private static readonly Regex RegexSize = new (@"\&xl=(?<size>\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public SubsPleaseParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) + private readonly NoAuthTorrentBaseSettings _settings; + + public SubsPleaseParser(NoAuthTorrentBaseSettings settings) { _settings = settings; - _categories = categories; } public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) @@ -198,7 +204,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var release = new TorrentInfo { - InfoUrl = _settings.BaseUrl + $"shows/{value.Page}/", + InfoUrl = $"{_settings.BaseUrl}shows/{value.Page}/", PublishDate = value.ReleaseDate.LocalDateTime, Files = 1, Categories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime }, @@ -210,34 +216,22 @@ namespace NzbDrone.Core.Indexers.Definitions UploadVolumeFactor = 1 }; + if (value.ImageUrl.IsNotNullOrWhiteSpace()) + { + release.PosterUrl = _settings.BaseUrl + value.ImageUrl.TrimStart('/'); + } + if (value.Episode.ToLowerInvariant() == "movie") { release.Categories.Add(NewznabStandardCategory.MoviesOther); } // Ex: [SubsPlease] Shingeki no Kyojin (The Final Season) - 64 (1080p) - release.Title += $"[SubsPlease] {value.Show} - {value.Episode} ({d.Res}p)"; + release.Title = $"[SubsPlease] {value.Show} - {value.Episode} ({d.Resolution}p)"; release.MagnetUrl = d.Magnet; release.DownloadUrl = null; release.Guid = d.Magnet; - - // The API doesn't tell us file size, so give an estimate based on resolution - if (string.Equals(d.Res, "1080")) - { - release.Size = 1395864371; // 1.3GB - } - else if (string.Equals(d.Res, "720")) - { - release.Size = 734003200; // 700MB - } - else if (string.Equals(d.Res, "480")) - { - release.Size = 367001600; // 350MB - } - else - { - release.Size = 1073741824; // 1GB - } + release.Size = GetReleaseSize(d); torrentInfos.Add(release); } @@ -246,6 +240,30 @@ namespace NzbDrone.Core.Indexers.Definitions return torrentInfos.ToArray(); } + private static long GetReleaseSize(SubPleaseDownloadInfo info) + { + if (info.Magnet.IsNotNullOrWhiteSpace()) + { + var sizeMatch = RegexSize.Match(info.Magnet); + + if (sizeMatch.Success && + long.TryParse(sizeMatch.Groups["size"].Value, out var releaseSize) + && releaseSize > 0) + { + return releaseSize; + } + } + + // The API doesn't tell us file size, so give an estimate based on resolution + return info.Resolution switch + { + "1080" => 1.3.Gigabytes(), + "720" => 700.Megabytes(), + "480" => 350.Megabytes(), + _ => 1.Gigabytes() + }; + } + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } } @@ -259,13 +277,16 @@ namespace NzbDrone.Core.Indexers.Definitions public string Episode { get; set; } public SubPleaseDownloadInfo[] Downloads { get; set; } public string Xdcc { get; set; } + + [JsonProperty("image_url")] public string ImageUrl { get; set; } public string Page { get; set; } } public class SubPleaseDownloadInfo { - public string Res { get; set; } + [JsonProperty("res")] + public string Resolution { get; set; } public string Magnet { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/TVVault.cs b/src/NzbDrone.Core/Indexers/Definitions/TVVault.cs deleted file mode 100644 index 4b75923a0..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/TVVault.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Web; -using AngleSharp.Html.Parser; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Indexers.Settings; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.Definitions -{ - [Obsolete("Remove per Site Request Prowlarr Issue 573")] - public class TVVault : TorrentIndexerBase<UserPassTorrentBaseSettings> - { - public override string Name => "TVVault"; - public override string[] IndexerUrls => new[] { "https://tv-vault.me/" }; - private string LoginUrl => Settings.BaseUrl + "login.php"; - public override string Description => "TV-Vault is a very unique tracker dedicated for old TV shows, TV movies and documentaries."; - public override string Language => "en-US"; - public override Encoding Encoding => Encoding.UTF8; - public override IndexerPrivacy Privacy => IndexerPrivacy.Private; - public override IndexerCapabilities Capabilities => SetCapabilities(); - public override TimeSpan RateLimit => TimeSpan.FromSeconds(5); - - public TVVault(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) - : base(httpClient, eventAggregator, indexerStatusService, configService, logger) - { - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new TVVaultRequestGenerator { Settings = Settings, Capabilities = Capabilities }; - } - - public override IParseIndexerResponse GetParser() - { - return new TVVaultParser(Settings, Capabilities.Categories); - } - - protected override async Task DoLogin() - { - var requestBuilder = new HttpRequestBuilder(LoginUrl) - { - LogResponseContent = true, - AllowAutoRedirect = true, - Method = HttpMethod.Post - }; - - var cookies = Cookies; - Cookies = null; - - var authLoginRequest = requestBuilder - .AddFormParameter("username", Settings.Username) - .AddFormParameter("password", Settings.Password) - .AddFormParameter("keeplogged", "1") - .AddFormParameter("login", "Log+In!") - .SetHeader("Content-Type", "application/x-www-form-urlencoded") - .SetHeader("Referer", LoginUrl) - .Build(); - - var response = await ExecuteAuth(authLoginRequest); - - if (CheckIfLoginNeeded(response)) - { - var parser = new HtmlParser(); - using var dom = parser.ParseDocument(response.Content); - var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim(); - - throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report."); - } - - cookies = response.GetCookies(); - UpdateCookies(cookies, DateTime.Now.AddDays(30)); - - _logger.Debug("TVVault authentication succeeded."); - } - - protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) - { - return !httpResponse.Content.Contains("logout.php"); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List<TvSearchParam> - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId - }, - MovieSearchParams = new List<MovieSearchParam> - { - MovieSearchParam.Q, MovieSearchParam.ImdbId - }, - Flags = new List<IndexerFlag> - { - IndexerFlag.FreeLeech - } - }; - - caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV); - caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Movies); - caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVHD); - caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVSD); - - return caps; - } - } - - public class TVVaultRequestGenerator : IIndexerRequestGenerator - { - public UserPassTorrentBaseSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - - private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null) - { - var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/')); - - var qc = new NameValueCollection - { - { "order_by", "s3" }, - { "order_way", "DESC" }, - { "disablegrouping", "1" } - }; - - if (imdbId.IsNotNullOrWhiteSpace()) - { - qc.Add("action", "advanced"); - qc.Add("imdbid", imdbId); - } - else if (!string.IsNullOrWhiteSpace(term)) - { - qc.Add("searchstr", StripSearchString(term)); - } - - var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories); - - foreach (var cat in catList) - { - qc.Add($"filter_cat[{cat}]", "1"); - } - - searchUrl = searchUrl + "?" + qc.GetQueryString(); - - var request = new IndexerRequest(searchUrl, HttpAccept.Html); - - yield return request; - } - - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId)); - - return pageableRequests; - } - - public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); - - return pageableRequests; - } - - private string StripSearchString(string term) - { - // Search does not support searching with episode numbers so strip it if we have one - // AND filter the result later to achieve the proper result - term = Regex.Replace(term, @"[S|E]\d\d", string.Empty); - return term.Trim(); - } - - public Func<IDictionary<string, string>> GetCookies { get; set; } - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } - } - - public class TVVaultParser : IParseIndexerResponse - { - private readonly UserPassTorrentBaseSettings _settings; - private readonly IndexerCapabilitiesCategories _categories; - - public TVVaultParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories) - { - _settings = settings; - _categories = categories; - } - - public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) - { - var torrentInfos = new List<ReleaseInfo>(); - - var parser = new HtmlParser(); - using var doc = parser.ParseDocument(indexerResponse.Content); - - // get params to build download link (user could be banned without those params) - var rssFeedUri = new Uri(_settings.BaseUrl + doc.QuerySelector("link[href^=\"/feeds.php?feed=\"]") - .GetAttribute("href")); - var rssFeedQuery = HttpUtility.ParseQueryString(rssFeedUri.Query); - var downloadLinkExtraParams = "&authkey=" + rssFeedQuery["authkey"] + "&torrent_pass=" + rssFeedQuery["passkey"]; - - var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent"); - - foreach (var row in rows) - { - var qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]"); - var title = qDetailsLink.TextContent; - - var description = qDetailsLink.NextSibling.TextContent.Trim(); - title += " " + description; - var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href"); - var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last(); - var link = _settings.BaseUrl + "torrents.php?action=download&id=" + torrentId + downloadLinkExtraParams; - - var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent); - var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent); - var size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent); - var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent); - var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent); - var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent); - - var dlVolumeFactor = row.QuerySelector("strong.freeleech_normal") != null ? 0 : 1; - - var category = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(description) }; - - var release = new TorrentInfo - { - MinimumRatio = 1, - MinimumSeedTime = 0, - Description = description, - Title = title, - PublishDate = publishDate, - Categories = category, - DownloadUrl = link, - InfoUrl = details, - Guid = link, - Seeders = seeders, - Peers = leechers + seeders, - Size = size, - Grabs = grabs, - Files = files, - DownloadVolumeFactor = dlVolumeFactor, - UploadVolumeFactor = 1 - }; - - torrentInfos.Add(release); - } - - return torrentInfos.ToArray(); - } - - public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Toloka.cs b/src/NzbDrone.Core/Indexers/Definitions/Toloka.cs index 2e4955699..ea1f5f9af 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Toloka.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Toloka.cs @@ -110,7 +110,8 @@ namespace NzbDrone.Core.Indexers.Definitions BookSearchParams = new List<BookSearchParam> { BookSearchParam.Q - } + }, + SupportsRawSearch = true }; caps.Categories.AddCategoryMapping("117", NewznabStandardCategory.Movies, "Українське кіно"); diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentDay.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentDay.cs index b8510edf7..aa02c4578 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentDay.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentDay.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Annotations; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; @@ -20,16 +21,21 @@ namespace NzbDrone.Core.Indexers.Definitions public override string Name => "TorrentDay"; public override string[] IndexerUrls => new[] { - "https://torrentday.cool/", "https://tday.love/", + "https://torrentday.cool/", "https://secure.torrentday.com/", "https://classic.torrentday.com/", "https://www.torrentday.com/", + "https://www.torrentday.me/", "https://torrentday.it/", "https://td.findnemo.net/", "https://td.getcrazy.me/", "https://td.venom.global/", - "https://td.workisboring.net/" + "https://td.workisboring.net/", + "https://tday.findnemo.net/", + "https://tday.getcrazy.me/", + "https://tday.venom.global/", + "https://tday.workisboring.net/" }; public override string Description => "TorrentDay (TD) is a Private site for TV / MOVIES / GENERAL"; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; @@ -47,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IParseIndexerResponse GetParser() { - return new TorrentDayParser(Settings, Capabilities.Categories); + return new TorrentDayParser(Settings, Capabilities.Categories, _logger); } protected override IDictionary<string, string> GetCookies() @@ -223,15 +229,29 @@ namespace NzbDrone.Core.Indexers.Definitions { private readonly TorrentDaySettings _settings; private readonly IndexerCapabilitiesCategories _categories; + private readonly Logger _logger; - public TorrentDayParser(TorrentDaySettings settings, IndexerCapabilitiesCategories categories) + public TorrentDayParser(TorrentDaySettings settings, IndexerCapabilitiesCategories categories, Logger logger) { _settings = settings; _categories = categories; + _logger = logger; } public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse) { + if (indexerResponse.HttpResponse.HasHttpRedirect) + { + _logger.Warn("Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl); + + if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("/login.php")) + { + throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie and try testing the indexer."); + } + + throw new IndexerException(indexerResponse, "Redirected to {0} from indexer request", indexerResponse.HttpResponse.RedirectUrl); + } + var torrentInfos = new List<TorrentInfo>(); var rows = JsonConvert.DeserializeObject<dynamic>(indexerResponse.Content); @@ -266,7 +286,7 @@ namespace NzbDrone.Core.Indexers.Definitions DownloadVolumeFactor = downloadMultiplier, UploadVolumeFactor = 1, MinimumRatio = 1, - MinimumSeedTime = 172800 // 48 hours + MinimumSeedTime = 259200 // 72 hours }; torrentInfos.Add(release); diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssIndexer.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssIndexer.cs index fff80eba4..c2afa00d2 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssIndexer.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssIndexer.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation.Results; using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; @@ -32,7 +34,7 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss public override IParseIndexerResponse GetParser() { - return _torrentRssParserFactory.GetParser(Settings); + return _torrentRssParserFactory.GetParser(Settings, Definition); } public override IEnumerable<ProviderDefinition> DefaultDefinitions @@ -44,6 +46,13 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss } } + protected override Task<ValidationFailure> TestConnection() + { + UpdateCookies(null, null); + + return base.TestConnection(); + } + private IndexerDefinition GetDefinition(string name, string description, TorrentRssIndexerSettings settings) { return new IndexerDefinition diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssParserFactory.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssParserFactory.cs index 5f5926a83..0a0e963ba 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssParserFactory.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssParserFactory.cs @@ -3,12 +3,13 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Serializer; using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Definitions.TorrentRss { public interface ITorrentRssParserFactory { - TorrentRssParser GetParser(TorrentRssIndexerSettings settings); + TorrentRssParser GetParser(TorrentRssIndexerSettings settings, ProviderDefinition definition); } public class TorrentRssParserFactory : ITorrentRssParserFactory @@ -26,10 +27,10 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss _logger = logger; } - public TorrentRssParser GetParser(TorrentRssIndexerSettings indexerSettings) + public TorrentRssParser GetParser(TorrentRssIndexerSettings indexerSettings, ProviderDefinition definition) { var key = indexerSettings.ToJson(); - var parserSettings = _settingsCache.Get(key, () => DetectParserSettings(indexerSettings), TimeSpan.FromDays(7)); + var parserSettings = _settingsCache.Get(key, () => DetectParserSettings(indexerSettings, definition), TimeSpan.FromDays(7)); if (parserSettings.UseEZTVFormat) { @@ -51,9 +52,9 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss }; } - private TorrentRssIndexerParserSettings DetectParserSettings(TorrentRssIndexerSettings indexerSettings) + private TorrentRssIndexerParserSettings DetectParserSettings(TorrentRssIndexerSettings indexerSettings, ProviderDefinition definition) { - var settings = _torrentRssSettingsDetector.Detect(indexerSettings); + var settings = _torrentRssSettingsDetector.Detect(indexerSettings, definition); if (settings == null) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssSettingsDetector.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssSettingsDetector.cs index 919b0c338..e1d1c2316 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssSettingsDetector.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentRss/TorrentRssSettingsDetector.cs @@ -9,28 +9,29 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Definitions.TorrentRss { public interface ITorrentRssSettingsDetector { - TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings); + TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings, ProviderDefinition definition); } public class TorrentRssSettingsDetector : ITorrentRssSettingsDetector { private const long ValidSizeThreshold = 2 * 1024 * 1024; - private readonly IHttpClient _httpClient; + private readonly IIndexerHttpClient _httpClient; private readonly Logger _logger; - public TorrentRssSettingsDetector(IHttpClient httpClient, Logger logger) + public TorrentRssSettingsDetector(IIndexerHttpClient httpClient, Logger logger) { _httpClient = httpClient; _logger = logger; } - public TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings) + public TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings, ProviderDefinition definition) { _logger.Debug("Evaluating TorrentRss feed '{0}'", settings.BaseUrl); @@ -43,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Definitions.TorrentRss try { - httpResponse = _httpClient.Execute(request.HttpRequest); + httpResponse = _httpClient.ExecuteProxied(request.HttpRequest, definition); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentSyndikat.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentSyndikat.cs index 6cd9dc8ae..53be8ec86 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentSyndikat.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentSyndikat.cs @@ -336,7 +336,7 @@ namespace NzbDrone.Core.Indexers.Definitions ReleaseTypes = new List<int>(); } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "Site API Key")] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "IndexerTorrentSyndikatSettingsApiKeyHelpText")] public string ApiKey { get; set; } [FieldDefinition(3, Label = "Products Only", Type = FieldType.Checkbox, HelpText = "Limit search to torrents linked to a product")] diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentsCSV.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentsCSV.cs index 2cf09f51e..80835a3ec 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/TorrentsCSV.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentsCSV.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; -using Newtonsoft.Json.Linq; +using System.Text.Json.Serialization; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.IndexerSearch.Definitions; @@ -19,7 +20,8 @@ namespace NzbDrone.Core.Indexers.Definitions public class TorrentsCSV : TorrentIndexerBase<NoAuthTorrentBaseSettings> { public override string Name => "TorrentsCSV"; - public override string[] IndexerUrls => new[] { "https://torrents-csv.ml/" }; + public override string[] IndexerUrls => new[] { "https://torrents-csv.com/" }; + public override string[] LegacyUrls => new[] { "https://torrents-csv.ml/" }; public override string Language => "en-US"; public override string Description => "Torrents.csv is a self-hostable open source torrent search engine and database"; public override Encoding Encoding => Encoding.UTF8; @@ -144,32 +146,31 @@ namespace NzbDrone.Core.Indexers.Definitions { var releaseInfos = new List<ReleaseInfo>(); - var jsonContent = JArray.Parse(indexerResponse.Content); + var jsonResponse = STJson.Deserialize<TorrentsCSVResponse>(indexerResponse.Content); - foreach (var torrent in jsonContent) + foreach (var torrent in jsonResponse.Torrents) { if (torrent == null) { continue; } - var infoHash = torrent.Value<string>("infohash"); - var title = torrent.Value<string>("name"); - var size = torrent.Value<long>("size_bytes"); - var seeders = torrent.Value<int?>("seeders") ?? 0; - var leechers = torrent.Value<int?>("leechers") ?? 0; - var grabs = torrent.Value<int?>("completed") ?? 0; - var publishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.Value<long>("created_unix")); + var infoHash = torrent.InfoHash; + var title = torrent.Name; + var seeders = torrent.Seeders ?? 0; + var leechers = torrent.Leechers ?? 0; + var grabs = torrent.Completed ?? 0; + var publishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.Created); var release = new TorrentInfo { - Title = title, - InfoUrl = $"{_settings.BaseUrl.TrimEnd('/')}/search/{title}", // there is no details link Guid = $"magnet:?xt=urn:btih:{infoHash}", + InfoUrl = $"{_settings.BaseUrl.TrimEnd('/')}/search?q={title}", // there is no details link + Title = title, InfoHash = infoHash, // magnet link is auto generated from infohash Categories = new List<IndexerCategory> { NewznabStandardCategory.Other }, PublishDate = publishDate, - Size = size, + Size = torrent.Size, Grabs = grabs, Seeders = seeders, Peers = leechers + seeders, @@ -187,4 +188,29 @@ namespace NzbDrone.Core.Indexers.Definitions public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } } + + public class TorrentsCSVResponse + { + public IReadOnlyCollection<TorrentsCSVTorrent> Torrents { get; set; } + } + + public class TorrentsCSVTorrent + { + [JsonPropertyName("infohash")] + public string InfoHash { get; set; } + + public string Name { get; set; } + + [JsonPropertyName("size_bytes")] + public long Size { get; set; } + + [JsonPropertyName("created_unix")] + public long Created { get; set; } + + public int? Leechers { get; set; } + + public int? Seeders { get; set; } + + public int? Completed { get; set; } + } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs index 512aaf091..b41640225 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/Torznab.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Torznab public string[] GetBaseUrlFromSettings() { - if (Definition == null || Settings?.Categories == null) + if (Definition == null || Settings?.Capabilities == null) { return new[] { "" }; } @@ -61,16 +61,23 @@ namespace NzbDrone.Core.Indexers.Torznab { var caps = new IndexerCapabilities(); - if (Definition == null || Settings?.Categories == null) + if (Definition == null || Settings?.Capabilities?.Categories == null) { return caps; } - foreach (var category in Settings.Categories) + foreach (var category in Settings.Capabilities.Categories) { caps.Categories.AddCategoryMapping(category.Name, category); } + caps.SupportsRawSearch = Settings?.Capabilities?.SupportsRawSearch ?? false; + caps.SearchParams = Settings?.Capabilities?.SearchParams ?? new List<SearchParam> { SearchParam.Q }; + caps.TvSearchParams = Settings?.Capabilities?.TvSearchParams ?? new List<TvSearchParam>(); + caps.MovieSearchParams = Settings?.Capabilities?.MovieSearchParams ?? new List<MovieSearchParam>(); + caps.MusicSearchParams = Settings?.Capabilities?.MusicSearchParams ?? new List<MusicSearchParam>(); + caps.BookSearchParams = Settings?.Capabilities?.BookSearchParams ?? new List<BookSearchParam>(); + return caps; } @@ -84,8 +91,8 @@ namespace NzbDrone.Core.Indexers.Torznab { get { - yield return GetDefinition("AnimeTosho", "Anime NZB/DDL mirror", settings: GetSettings("https://feed.animetosho.org")); - yield return GetDefinition("MoreThanTV", "Private torrent tracker for TV / MOVIES", settings: GetSettings("https://www.morethantv.me", apiPath: @"/api/torznab")); + 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("")); } @@ -97,8 +104,23 @@ namespace NzbDrone.Core.Indexers.Torznab _capabilitiesProvider = capabilitiesProvider; } - private IndexerDefinition GetDefinition(string name, string description, string language = null, TorznabSettings settings = null) + private IndexerDefinition GetDefinition(string name, string description, string language = null, TorznabSettings settings = null, IEnumerable<int> categories = null) { + var caps = new IndexerCapabilities(); + + if (categories != null) + { + foreach (var categoryId in categories) + { + var mappedCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == categoryId); + + if (mappedCat != null) + { + caps.Categories.AddCategoryMapping(mappedCat.Id, mappedCat); + } + } + } + return new IndexerDefinition { Enable = true, @@ -112,7 +134,7 @@ namespace NzbDrone.Core.Indexers.Torznab SupportsSearch = SupportsSearch, SupportsRedirect = SupportsRedirect, SupportsPagination = SupportsPagination, - Capabilities = Capabilities + Capabilities = caps }; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabRssParser.cs index 9bd544aad..48ebe4846 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabRssParser.cs @@ -100,16 +100,17 @@ namespace NzbDrone.Core.Indexers.Torznab protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases) { var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray(); + if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty()) { if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any()) { - _logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Newznab indexer?", TorrentEnclosureMimeType, enclosureTypes[0]); - } - else - { - _logger.Warn("Feed does not contain {0}, found {1}.", TorrentEnclosureMimeType, enclosureTypes[0]); + _logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Newznab indexer?", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]); + + return false; } + + _logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]); } return true; @@ -277,6 +278,18 @@ namespace NzbDrone.Core.Indexers.Torznab flags.Add(IndexerFlag.FreeLeech); } + var tags = TryGetMultipleTorznabAttributes(item, "tag"); + + if (tags.Any(t => t.EqualsIgnoreCase("internal"))) + { + flags.Add(IndexerFlag.Internal); + } + + if (tags.Any(t => t.EqualsIgnoreCase("scene"))) + { + flags.Add(IndexerFlag.Scene); + } + return flags; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs index 31b4cac17..ca461467c 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Torznab/TorznabSettings.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; @@ -10,10 +11,7 @@ namespace NzbDrone.Core.Indexers.Torznab { public class TorznabSettingsValidator : AbstractValidator<TorznabSettings> { - private static readonly string[] ApiKeyWhiteList = - { - "hd4free.xyz", - }; + private static readonly string[] ApiKeyWhiteList = Array.Empty<string>(); private static bool ShouldHaveApiKey(TorznabSettings settings) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs index eac5ccf22..57fd7d070 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Indexers.Definitions.UNIT3D { private static readonly Unit3dSettingsValidator Validator = new (); - [FieldDefinition(2, Label = "API Key", HelpText = "Site API Key generated in My Security", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", HelpText = "Site API Key generated in My Security", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Definitions/Uniotaku.cs b/src/NzbDrone.Core/Indexers/Definitions/Uniotaku.cs index 9405e63c3..12eca92f6 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Uniotaku.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Uniotaku.cs @@ -82,7 +82,7 @@ public class Uniotaku : TorrentIndexerBase<UniotakuSettings> return !httpResponse.GetCookies().ContainsKey("uid") || !httpResponse.GetCookies().ContainsKey("pass"); } - public override async Task<byte[]> Download(Uri link) + public override async Task<IndexerDownloadResponse> Download(Uri link) { var request = new HttpRequestBuilder(link.ToString()) .SetCookies(GetCookies() ?? new Dictionary<string, string>()) @@ -137,6 +137,7 @@ public class Uniotaku : TorrentIndexerBase<UniotakuSettings> caps.Categories.AddCategoryMapping(55, NewznabStandardCategory.XXX, "Hentai"); caps.Categories.AddCategoryMapping(56, NewznabStandardCategory.XXXOther, "H Doujinshi"); caps.Categories.AddCategoryMapping(57, NewznabStandardCategory.TVOther, "Tokusatsu"); + caps.Categories.AddCategoryMapping(58, NewznabStandardCategory.TVOther, "Live Action"); return caps; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/XSpeeds.cs b/src/NzbDrone.Core/Indexers/Definitions/XSpeeds.cs index 99efc2ecc..72d4f1c2b 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/XSpeeds.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/XSpeeds.cs @@ -120,8 +120,16 @@ public class XSpeeds : TorrentIndexerBase<XSpeedsSettings> caps.Categories.AddCategoryMapping(113, NewznabStandardCategory.TVAnime, "Anime Boxsets"); caps.Categories.AddCategoryMapping(112, NewznabStandardCategory.MoviesOther, "Anime Movies"); caps.Categories.AddCategoryMapping(111, NewznabStandardCategory.MoviesOther, "Anime TV"); - caps.Categories.AddCategoryMapping(80, NewznabStandardCategory.AudioAudiobook, "Audiobooks"); - caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.Books, "Books Magazines"); + caps.Categories.AddCategoryMapping(150, NewznabStandardCategory.PC, "Apps"); + caps.Categories.AddCategoryMapping(156, NewznabStandardCategory.TV, "AV1"); + caps.Categories.AddCategoryMapping(156, NewznabStandardCategory.Movies, "AV1"); + caps.Categories.AddCategoryMapping(159, NewznabStandardCategory.Movies, "Movie Boxsets AV1"); + caps.Categories.AddCategoryMapping(158, NewznabStandardCategory.Movies, "Movies AV1"); + caps.Categories.AddCategoryMapping(157, NewznabStandardCategory.TV, "TV AV1"); + caps.Categories.AddCategoryMapping(160, NewznabStandardCategory.TV, "TV Boxsets AV1"); + caps.Categories.AddCategoryMapping(153, NewznabStandardCategory.Books, "Books"); + caps.Categories.AddCategoryMapping(154, NewznabStandardCategory.AudioAudiobook, "Audiobooks"); + caps.Categories.AddCategoryMapping(155, NewznabStandardCategory.Books, "Books & Magazines"); caps.Categories.AddCategoryMapping(68, NewznabStandardCategory.MoviesOther, "Cams/TS"); caps.Categories.AddCategoryMapping(140, NewznabStandardCategory.TVDocumentary, "Documentary"); caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.MoviesDVD, "DVDR"); @@ -153,8 +161,9 @@ public class XSpeeds : TorrentIndexerBase<XSpeedsSettings> caps.Categories.AddCategoryMapping(146, NewznabStandardCategory.MoviesSD, "Movies SD"); caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.Audio, "Music"); caps.Categories.AddCategoryMapping(135, NewznabStandardCategory.AudioLossless, "Music/FLAC"); + caps.Categories.AddCategoryMapping(151, NewznabStandardCategory.Audio, "Karaoke"); caps.Categories.AddCategoryMapping(136, NewznabStandardCategory.Audio, "Music Boxset"); - caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.AudioVideo, "Music Videos"); + caps.Categories.AddCategoryMapping(148, NewznabStandardCategory.AudioVideo, "Music Videos"); caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.Other, "Other"); caps.Categories.AddCategoryMapping(125, NewznabStandardCategory.Other, "Other/Pictures"); caps.Categories.AddCategoryMapping(54, NewznabStandardCategory.TVOther, "Soaps"); @@ -168,6 +177,7 @@ public class XSpeeds : TorrentIndexerBase<XSpeedsSettings> caps.Categories.AddCategoryMapping(86, NewznabStandardCategory.TVSport, "Sports/MotorSports"); caps.Categories.AddCategoryMapping(89, NewznabStandardCategory.TVSport, "Sports/Olympics"); caps.Categories.AddCategoryMapping(126, NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping(149, NewznabStandardCategory.TV, "TV Specials"); caps.Categories.AddCategoryMapping(127, NewznabStandardCategory.TVUHD, "TV 4K"); caps.Categories.AddCategoryMapping(129, NewznabStandardCategory.TVHD, "TV HD"); caps.Categories.AddCategoryMapping(130, NewznabStandardCategory.TVHD, "TV HEVC"); diff --git a/src/NzbDrone.Core/Indexers/Events/IndexerAuthEvent.cs b/src/NzbDrone.Core/Indexers/Events/IndexerAuthEvent.cs index ed515b3b0..9a8888ae1 100644 --- a/src/NzbDrone.Core/Indexers/Events/IndexerAuthEvent.cs +++ b/src/NzbDrone.Core/Indexers/Events/IndexerAuthEvent.cs @@ -6,13 +6,13 @@ namespace NzbDrone.Core.Indexers.Events { public int IndexerId { get; set; } public bool Successful { get; set; } - public long Time { get; set; } + public long ElapsedTime { get; set; } - public IndexerAuthEvent(int indexerId, bool successful, long time) + public IndexerAuthEvent(int indexerId, bool successful, long elapsedTime) { IndexerId = indexerId; Successful = successful; - Time = time; + ElapsedTime = elapsedTime; } } } diff --git a/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs b/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs index 43bf8cd60..d68397562 100644 --- a/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs +++ b/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Indexers.Events public string DownloadId { get; set; } public IIndexer Indexer { get; set; } public GrabTrigger GrabTrigger { get; set; } + public long ElapsedTime { get; set; } public IndexerDownloadEvent(ReleaseInfo release, bool successful, string source, string host, string title, string url) { diff --git a/src/NzbDrone.Core/Indexers/Exceptions/IndexerException.cs b/src/NzbDrone.Core/Indexers/Exceptions/IndexerException.cs index 629f596a8..f0f1c2899 100644 --- a/src/NzbDrone.Core/Indexers/Exceptions/IndexerException.cs +++ b/src/NzbDrone.Core/Indexers/Exceptions/IndexerException.cs @@ -1,23 +1,34 @@ -using NzbDrone.Common.Exceptions; +using System; +using NzbDrone.Common.Exceptions; namespace NzbDrone.Core.Indexers.Exceptions { public class IndexerException : NzbDroneException { - private readonly IndexerResponse _indexerResponse; - - public IndexerException(IndexerResponse response, string message, params object[] args) - : base(message, args) - { - _indexerResponse = response; - } + public IndexerResponse Response { get; } public IndexerException(IndexerResponse response, string message) : base(message) { - _indexerResponse = response; + Response = response; } - public IndexerResponse Response => _indexerResponse; + public IndexerException(IndexerResponse response, string message, Exception innerException) + : base(message, innerException) + { + Response = response; + } + + public IndexerException(IndexerResponse response, string message, params object[] args) + : base(message, args) + { + Response = response; + } + + public IndexerException(IndexerResponse response, string message, Exception innerException, params object[] args) + : base(message, innerException, args) + { + Response = response; + } } } diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 6a6b965b3..4a8014c63 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -18,6 +18,9 @@ using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; +using Polly; +using Polly.Retry; namespace NzbDrone.Core.Indexers { @@ -25,9 +28,43 @@ namespace NzbDrone.Core.Indexers where TSettings : IIndexerSettings, new() { protected const int MaxNumResultsPerQuery = 1000; + private const int MaxRedirects = 5; protected readonly IIndexerHttpClient _httpClient; protected readonly IEventAggregator _eventAggregator; + + protected ResiliencePipeline<HttpResponse> RetryStrategy => new ResiliencePipelineBuilder<HttpResponse>() + .AddRetry(new RetryStrategyOptions<HttpResponse> + { + ShouldHandle = static args => args.Outcome switch + { + { Result.HasHttpServerError: true } => PredicateResult.True(), + { Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(), + { Exception: HttpException { Response.HasHttpServerError: true } } => PredicateResult.True(), + _ => PredicateResult.False() + }, + Delay = RateLimit, + MaxRetryAttempts = 2, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + OnRetry = args => + { + var exception = args.Outcome.Exception; + + if (exception is not null) + { + _logger.Warn(exception, "Request for {0} failed with exception '{1}'. Retrying in {2}s.", Definition.Name, exception.Message, args.RetryDelay.TotalSeconds); + } + else + { + _logger.Warn("Request for {0} failed with status {1}. Retrying in {2}s.", Definition.Name, args.Outcome.Result?.StatusCode, args.RetryDelay.TotalSeconds); + } + + return default; + } + }) + .Build(); + public IDictionary<string, string> Cookies { get; set; } public override bool SupportsRss => true; @@ -187,7 +224,7 @@ namespace NzbDrone.Core.Indexers return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria), searchCriteria); } - public override async Task<byte[]> Download(Uri link) + public override async Task<IndexerDownloadResponse> Download(Uri link) { Cookies = GetCookies(); @@ -196,7 +233,7 @@ namespace NzbDrone.Core.Indexers if (request.Url.Scheme == "magnet") { ValidateMagnet(request.Url.FullUri); - return Encoding.UTF8.GetBytes(request.Url.FullUri); + return new IndexerDownloadResponse(Encoding.UTF8.GetBytes(request.Url.FullUri)); } if (request.RateLimit < RateLimit) @@ -204,12 +241,54 @@ namespace NzbDrone.Core.Indexers request.RateLimit = RateLimit; } + request.AllowAutoRedirect = false; + byte[] fileData; + long elapsedTime; try { var response = await _httpClient.ExecuteProxiedAsync(request, Definition); + + if (response.StatusCode is HttpStatusCode.MovedPermanently + or HttpStatusCode.Found + or HttpStatusCode.SeeOther + or HttpStatusCode.TemporaryRedirect + or HttpStatusCode.PermanentRedirect) + { + var autoRedirectChain = new List<string> { request.Url.ToString() }; + + do + { + var redirectUrl = response.RedirectUrl; + + _logger.Debug("Download request is being redirected to: {0}", redirectUrl); + + if (redirectUrl.IsNullOrWhiteSpace()) + { + throw new WebException("Remote website tried to redirect without providing a location."); + } + + if (redirectUrl.StartsWith("magnet:")) + { + return await Download(new Uri(redirectUrl)); + } + + request.Url = new HttpUri(redirectUrl); + autoRedirectChain.Add(request.Url.ToString()); + + if (autoRedirectChain.Count > MaxRedirects) + { + throw new WebException($"Too many download redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError); + } + + response = await _httpClient.ExecuteProxiedAsync(request, Definition); + } + while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.SeeOther); + } + fileData = response.ResponseData; + elapsedTime = response.ElapsedTime; _logger.Debug("Downloaded for release finished ({0} bytes from {1})", fileData.Length, link.AbsoluteUri); } @@ -247,15 +326,12 @@ namespace NzbDrone.Core.Indexers ValidateDownloadData(fileData); - return fileData; + return new IndexerDownloadResponse(fileData, elapsedTime); } protected virtual Task<HttpRequest> GetDownloadRequest(Uri link) { - var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri) - { - AllowAutoRedirect = FollowRedirect - }; + var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri); if (Cookies != null) { @@ -363,7 +439,7 @@ namespace NzbDrone.Core.Indexers } } - releases.AddRange(pagedReleases.Where(IsValidRelease)); + releases.AddRange(pagedReleases.Where(r => IsValidRelease(r, searchCriteria.InteractiveSearch))); } if (releases.Any()) @@ -388,11 +464,11 @@ namespace NzbDrone.Core.Indexers if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("504") || webException.Message.Contains("timed out")) { - _logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message); + _logger.Warn(webException, "{0} server is currently unavailable. {1} {2}", this, url, webException.Message); } else { - _logger.Warn("{0} {1} {2}", this, url, webException.Message); + _logger.Warn(webException, "{0} {1} {2}", this, url, webException.Message); } } catch (TooManyRequestsException ex) @@ -402,7 +478,7 @@ namespace NzbDrone.Core.Indexers var retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : minimumBackoff; _indexerStatusService.RecordFailure(Definition.Id, retryTime); - _logger.Warn("Request Limit reached for {0}. Disabled for {1}", this, retryTime); + _logger.Warn(ex, "Request Limit reached for {0}. Disabled for {1}", this, retryTime); } catch (HttpException ex) { @@ -411,18 +487,18 @@ namespace NzbDrone.Core.Indexers if (ex.Response.HasHttpServerError) { - _logger.Warn("Unable to connect to {0} at [{1}]. Indexer's server is unavailable. Try again later. {2}", this, url, ex.Message); + _logger.Warn(ex, "Unable to connect to {0} at [{1}]. Indexer's server is unavailable. Try again later. {2}", this, url, ex.Message); } else { - _logger.Warn("{0} {1}", this, ex.Message); + _logger.Warn(ex, "{0} {1}", this, ex.Message); } } catch (RequestLimitReachedException ex) { result.Queries.Add(new IndexerQueryResult { Response = ex.Response.HttpResponse }); _indexerStatusService.RecordFailure(Definition.Id, minimumBackoff); - _logger.Warn("Request Limit reached for {0}. Disabled for {1}", this, minimumBackoff); + _logger.Warn(ex, "Request Limit reached for {0}. Disabled for {1}", this, minimumBackoff); } catch (IndexerAuthException ex) { @@ -469,7 +545,7 @@ namespace NzbDrone.Core.Indexers return Capabilities ?? ((IndexerDefinition)Definition).Capabilities; } - protected virtual bool IsValidRelease(ReleaseInfo release) + protected virtual bool IsValidRelease(ReleaseInfo release, bool interactiveSearch = false) { if (release.Title.IsNullOrWhiteSpace()) { @@ -478,6 +554,26 @@ namespace NzbDrone.Core.Indexers return false; } + if (interactiveSearch) + { + // Show releases with issues in the interactive search + return true; + } + + if (release.Size == null) + { + _logger.Warn("Invalid Release: '{0}' from indexer: {1}. No size provided.", release.Title, Definition.Name); + + return false; + } + + if (release.Categories == null || !release.Categories.Any()) + { + _logger.Warn("Invalid Release: '{0}' from indexer: {1}. No categories provided.", release.Title, Definition.Name); + + return false; + } + return true; } @@ -515,12 +611,7 @@ namespace NzbDrone.Core.Indexers protected virtual bool CheckIfLoginNeeded(HttpResponse httpResponse) { - if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) - { - return true; - } - - return false; + return httpResponse.StatusCode == HttpStatusCode.Unauthorized; } protected virtual Task DoLogin() @@ -563,14 +654,16 @@ namespace NzbDrone.Core.Indexers { foreach (var cookie in Cookies) { - request.HttpRequest.Cookies.Add(cookie.Key, cookie.Value); + request.HttpRequest.Cookies[cookie.Key] = cookie.Value; } } request.HttpRequest.SuppressHttpError = true; request.HttpRequest.Encoding ??= Encoding; - var response = await _httpClient.ExecuteProxiedAsync(request.HttpRequest, Definition); + var response = await RetryStrategy + .ExecuteAsync(static async (state, _) => await state._httpClient.ExecuteProxiedAsync(state.HttpRequest, state.Definition), (_httpClient, request.HttpRequest, Definition)) + .ConfigureAwait(false); // Check response to see if auth is needed, if needed try again if (CheckIfLoginNeeded(response)) @@ -672,7 +765,7 @@ namespace NzbDrone.Core.Indexers if (releases.Releases.Empty()) { - return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer, your indexer category settings, or other indexer settings such as search freeleech only etc."); + return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer, your indexer category settings, or other indexer settings such as search freeleech only etc. See the FAQ for details."); } } catch (IndexerAuthException ex) @@ -723,7 +816,10 @@ namespace NzbDrone.Core.Indexers { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message) + { + DetailedDescription = ex.InnerException?.Message + }; } catch (TaskCanceledException ex) { @@ -733,7 +829,7 @@ namespace NzbDrone.Core.Indexers } catch (WebException webException) { - _logger.Warn("Unable to connect to indexer."); + _logger.Warn(webException, "Unable to connect to indexer."); if (webException.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure) { @@ -750,7 +846,10 @@ namespace NzbDrone.Core.Indexers { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message) + { + DetailedDescription = ex.InnerException?.Message + }; } return null; diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 25d12db9a..e0a6b1ef1 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers Task<IndexerPageableQueryResult> Fetch(BookSearchCriteria searchCriteria); Task<IndexerPageableQueryResult> Fetch(BasicSearchCriteria searchCriteria); - Task<byte[]> Download(Uri link); + Task<IndexerDownloadResponse> Download(Uri link); bool IsObsolete(); IndexerCapabilities GetCapabilities(); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 5f54dd620..0330217dc 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Indexers public abstract Task<IndexerPageableQueryResult> Fetch(TvSearchCriteria searchCriteria); public abstract Task<IndexerPageableQueryResult> Fetch(BookSearchCriteria searchCriteria); public abstract Task<IndexerPageableQueryResult> Fetch(BasicSearchCriteria searchCriteria); - public abstract Task<byte[]> Download(Uri link); + public abstract Task<IndexerDownloadResponse> Download(Uri link); public abstract IndexerCapabilities GetCapabilities(); @@ -138,6 +138,10 @@ namespace NzbDrone.Core.Indexers { torrentRelease.IndexerFlags.Add(IndexerFlag.FreeLeech); } + else if (torrentRelease.DownloadVolumeFactor == 0.5) + { + torrentRelease.IndexerFlags.Add(IndexerFlag.HalfLeech); + } if (torrentRelease.UploadVolumeFactor == 0.0) { @@ -167,10 +171,15 @@ namespace NzbDrone.Core.Indexers var splitRegex = new Regex("[^\\w]+"); // split search term to individual terms for less aggressive filtering, filter common terms - var terms = splitRegex.Split(searchCriteria.SearchTerm).Where(t => t.IsNotNullOrWhiteSpace() && t.Length > 1 && !commonWords.ContainsIgnoreCase(t)); + var terms = splitRegex.Split(searchCriteria.SearchTerm).Where(t => t.IsNotNullOrWhiteSpace() && t.Length > 1 && !commonWords.ContainsIgnoreCase(t)).ToArray(); // check in title and description for any term searched for - releases = releases.Where(r => terms.Any(t => (r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(t)) || (r.Description.IsNotNullOrWhiteSpace() && r.Description.ContainsIgnoreCase(t)))).ToList(); + releases = releases.Where(r => + { + var matches = terms.Where(t => (r.Title.IsNotNullOrWhiteSpace() && r.Title.ContainsIgnoreCase(t)) || (r.Description.IsNotNullOrWhiteSpace() && r.Description.ContainsIgnoreCase(t))); + + return terms.Length > 1 ? matches.Skip(1).Any() : matches.Any(); + }).ToList(); } return releases; diff --git a/src/NzbDrone.Core/Indexers/IndexerBaseSettings.cs b/src/NzbDrone.Core/Indexers/IndexerBaseSettings.cs index 0992cbacf..5bada9b42 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBaseSettings.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBaseSettings.cs @@ -21,13 +21,13 @@ namespace NzbDrone.Core.Indexers public class IndexerBaseSettings { - [FieldDefinition(1, Type = FieldType.Number, Label = "Query Limit", HelpText = "The number of max queries as specified by the respective unit that Prowlarr will allow to the site", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsQueryLimit", HelpText = "IndexerSettingsQueryLimitHelpText", Advanced = true)] public int? QueryLimit { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Grab Limit", HelpText = "The number of max grabs as specified by the respective unit that Prowlarr will allow to the site", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsGrabLimit", HelpText = "IndexerSettingsGrabLimitHelpText", Advanced = true)] public int? GrabLimit { get; set; } - [FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(IndexerLimitsUnit), Label = "Limits Unit", HelpText = "The unit of time for counting limits per indexer", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Select, Label = "IndexerSettingsLimitsUnit", SelectOptions = typeof(IndexerLimitsUnit), HelpText = "IndexerSettingsLimitsUnitHelpText", Advanced = true)] public int LimitsUnit { get; set; } = (int)IndexerLimitsUnit.Day; } diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs index a0b1df0a9..4b8a5f649 100644 --- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs +++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs @@ -29,8 +29,6 @@ namespace NzbDrone.Core.Indexers public int AppProfileId { get; set; } public LazyLoaded<AppSyncProfile> AppProfile { get; set; } - public IndexerStatus Status { get; set; } - - public List<SettingsField> ExtraFields { get; set; } = new List<SettingsField>(); + public List<SettingsField> ExtraFields { get; set; } = new (); } } diff --git a/src/NzbDrone.Core/Indexers/IndexerDownloadResponse.cs b/src/NzbDrone.Core/Indexers/IndexerDownloadResponse.cs new file mode 100644 index 000000000..a5660a784 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/IndexerDownloadResponse.cs @@ -0,0 +1,13 @@ +namespace NzbDrone.Core.Indexers; + +public class IndexerDownloadResponse +{ + public byte[] Data { get; private set; } + public long ElapsedTime { get; private set; } + + public IndexerDownloadResponse(byte[] data, long elapsedTime = 0) + { + Data = data; + ElapsedTime = elapsedTime; + } +} diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 53c2c2838..46a7e8c3e 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -55,10 +55,10 @@ namespace NzbDrone.Core.Indexers { MapCardigannDefinition(definition); } - catch + catch (Exception ex) { // Skip indexer if we fail in Cardigann mapping - _logger.Debug("Indexer '{0}' has no definition", definition.Name); + _logger.Debug(ex, "Indexer '{0}' has no definition", definition.Name); } } @@ -208,7 +208,7 @@ namespace NzbDrone.Core.Indexers definition.Description ??= provider.Description; definition.Encoding = provider.Encoding; definition.Language ??= provider.Language; - definition.Capabilities = provider.Capabilities; + definition.Capabilities ??= provider.Capabilities; } } @@ -242,7 +242,7 @@ namespace NzbDrone.Core.Indexers foreach (var indexer in indexers) { - if (blockedIndexers.TryGetValue(indexer.Definition.Id, out var blockedIndexerStatus)) + if (blockedIndexers.TryGetValue(indexer.Definition.Id, out var blockedIndexerStatus) && blockedIndexerStatus.DisabledTill.HasValue) { _logger.Debug("Temporarily ignoring indexer {0} till {1} due to recent failures.", indexer.Definition.Name, blockedIndexerStatus.DisabledTill.Value.ToLocalTime()); continue; @@ -284,7 +284,9 @@ namespace NzbDrone.Core.Indexers if (definition.Enable && definition.Implementation is nameof(Newznab.Newznab) or nameof(Torznab.Torznab)) { var settings = (NewznabSettings)definition.Settings; - settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList(); + var capabilities = _newznabCapabilitiesProvider.GetCapabilities(settings, definition); + + settings.Capabilities = new NewznabCapabilitiesSettings(capabilities); } if (definition.Implementation == nameof(Cardigann)) @@ -304,7 +306,9 @@ namespace NzbDrone.Core.Indexers if (definition.Enable && definition.Implementation is nameof(Newznab.Newznab) or nameof(Torznab.Torznab)) { var settings = (NewznabSettings)definition.Settings; - settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList(); + var capabilities = _newznabCapabilitiesProvider.GetCapabilities(settings, definition); + + settings.Capabilities = new NewznabCapabilitiesSettings(capabilities); } if (definition.Implementation == nameof(Cardigann)) @@ -314,5 +318,24 @@ namespace NzbDrone.Core.Indexers base.Update(definition); } + + public override IEnumerable<IndexerDefinition> Update(IEnumerable<IndexerDefinition> definitions) + { + var indexerDefinitions = definitions.ToList(); + + foreach (var definition in indexerDefinitions) + { + var provider = _providers.First(v => v.GetType().Name == definition.Implementation); + + SetProviderCharacteristics(provider, definition); + + if (definition.Implementation == nameof(Cardigann)) + { + MapCardigannDefinition(definition); + } + } + + return base.Update(indexerDefinitions); + } } } diff --git a/src/NzbDrone.Core/Indexers/IndexerFlag.cs b/src/NzbDrone.Core/Indexers/IndexerFlag.cs index 26a65e96c..cfa3d35de 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFlag.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFlag.cs @@ -63,6 +63,7 @@ namespace NzbDrone.Core.Indexers } public static IndexerFlag Internal => new ("internal", "Uploader is an internal release group"); + public static IndexerFlag Exclusive => new ("exclusive", "An exclusive release that must not be uploaded anywhere else"); public static IndexerFlag FreeLeech => new ("freeleech", "Download doesn't count toward ratio"); public static IndexerFlag NeutralLeech => new ("neutralleech", "Download and upload doesn't count toward ratio"); public static IndexerFlag HalfLeech => new ("halfleech", "Release counts 50% to ratio"); diff --git a/src/NzbDrone.Core/Indexers/IndexerHttpClient.cs b/src/NzbDrone.Core/Indexers/IndexerHttpClient.cs index ca822e25b..3f3705c58 100644 --- a/src/NzbDrone.Core/Indexers/IndexerHttpClient.cs +++ b/src/NzbDrone.Core/Indexers/IndexerHttpClient.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.TPL; @@ -34,56 +36,63 @@ namespace NzbDrone.Core.Indexers public async Task<HttpResponse> ExecuteProxiedAsync(HttpRequest request, ProviderDefinition definition) { - var selectedProxy = GetProxy(definition); + var selectedProxies = GetProxies(definition); - request = PreRequest(request, selectedProxy); + request = PreRequest(request, selectedProxies); - return PostResponse(await ExecuteAsync(request), selectedProxy); + return PostResponse(await ExecuteAsync(request), selectedProxies); } public HttpResponse ExecuteProxied(HttpRequest request, ProviderDefinition definition) { - var selectedProxy = GetProxy(definition); + var selectedProxies = GetProxies(definition); - request = PreRequest(request, selectedProxy); + request = PreRequest(request, selectedProxies); - return PostResponse(Execute(request), selectedProxy); + return PostResponse(Execute(request), selectedProxies); } - private IIndexerProxy GetProxy(ProviderDefinition definition) + private IList<IIndexerProxy> GetProxies(ProviderDefinition definition) { // Skip DB call if no tags on the indexers if (definition is { Id: > 0 } && definition.Tags.Count == 0) { - return null; + return Array.Empty<IIndexerProxy>(); } var proxies = _indexerProxyFactory.GetAvailableProviders(); - var selectedProxy = proxies.FirstOrDefault(proxy => definition.Tags.Intersect(proxy.Definition.Tags).Any()); - if (selectedProxy == null && definition is not { Id: not 0 }) + var selectedProxies = proxies + .Where(proxy => definition.Tags.Intersect(proxy.Definition.Tags).Any()) + .GroupBy(p => p is FlareSolverr) + .Select(g => g.First()) + .OrderBy(p => p is FlareSolverr) + .ToList(); + + if (!selectedProxies.Any() && definition is not { Id: not 0 }) { - selectedProxy = proxies.FirstOrDefault(p => p is FlareSolverr); + selectedProxies = new List<IIndexerProxy>(); + selectedProxies.AddIfNotNull(proxies.Find(p => p is FlareSolverr)); } - return selectedProxy; + return selectedProxies; } - private HttpRequest PreRequest(HttpRequest request, IIndexerProxy selectedProxy) + private HttpRequest PreRequest(HttpRequest request, IList<IIndexerProxy> selectedProxies) { - if (selectedProxy != null) + if (selectedProxies != null && selectedProxies.Any()) { - request = selectedProxy.PreRequest(request); + request = selectedProxies.Aggregate(request, (current, selectedProxy) => selectedProxy.PreRequest(current)); } return request; } - private HttpResponse PostResponse(HttpResponse response, IIndexerProxy selectedProxy) + private HttpResponse PostResponse(HttpResponse response, IList<IIndexerProxy> selectedProxies) { - if (selectedProxy != null) + if (selectedProxies != null && selectedProxies.Any()) { - response = selectedProxy.PostResponse(response); + response = selectedProxies.Aggregate(response, (current, selectedProxy) => selectedProxy.PostResponse(current)); } return response; diff --git a/src/NzbDrone.Core/Indexers/IndexerTorrentBaseSettings.cs b/src/NzbDrone.Core/Indexers/IndexerTorrentBaseSettings.cs index bae3f889e..7cdf4b311 100644 --- a/src/NzbDrone.Core/Indexers/IndexerTorrentBaseSettings.cs +++ b/src/NzbDrone.Core/Indexers/IndexerTorrentBaseSettings.cs @@ -52,16 +52,19 @@ namespace NzbDrone.Core.Indexers public class IndexerTorrentBaseSettings { - [FieldDefinition(1, Type = FieldType.Number, Label = "Apps Minimum Seeders", HelpText = "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsAppsMinimumSeeders", HelpText = "IndexerSettingsAppsMinimumSeedersHelpText", Advanced = true)] public int? AppMinimumSeeders { get; set; } - [FieldDefinition(2, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is app's default", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")] public double? SeedRatio { get; set; } - [FieldDefinition(3, Type = FieldType.Number, Label = "Seed Time", HelpText = "The time a torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)] public int? SeedTime { get; set; } - [FieldDefinition(4, Type = FieldType.Number, Label = "Pack Seed Time", HelpText = "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)] + [FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsPackSeedTime", HelpText = "IndexerSettingsPackSeedTimeIndexerHelpText", Unit = "minutes", Advanced = true)] public int? PackSeedTime { get; set; } + + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsPreferMagnetUrl", HelpText = "IndexerSettingsPreferMagnetUrlHelpText", Advanced = true)] + public bool PreferMagnetUrl { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/MagnetLinkBuilder.cs b/src/NzbDrone.Core/Indexers/MagnetLinkBuilder.cs index 23c0c8dc4..a16949e90 100644 --- a/src/NzbDrone.Core/Indexers/MagnetLinkBuilder.cs +++ b/src/NzbDrone.Core/Indexers/MagnetLinkBuilder.cs @@ -10,26 +10,26 @@ namespace NzbDrone.Core.Indexers { private static readonly List<string> Trackers = new () { - "udp://tracker.opentrackr.org:1337/announce", - "https://tracker2.ctix.cn:443/announce", - "https://tracker1.520.jp:443/announce", + "http://tracker.opentrackr.org:1337/announce", + "udp://tracker.auctor.tv:6969/announce", "udp://opentracker.i2p.rocks:6969/announce", - "udp://open.tracker.cl:1337/announce", + "https://opentracker.i2p.rocks:443/announce", "udp://open.demonii.com:1337/announce", "udp://tracker.openbittorrent.com:6969/announce", "http://tracker.openbittorrent.com:80/announce", "udp://open.stealth.si:80/announce", - "udp://exodus.desync.com:6969/announce", "udp://tracker.torrent.eu.org:451/announce", - "udp://tracker1.bt.moack.co.kr:80/announce", - "udp://tracker-udp.gbitt.info:80/announce", + "udp://tracker.moeking.me:6969/announce", "udp://explodie.org:6969/announce", - "https://tracker.gbitt.info:443/announce", - "http://tracker.gbitt.info:80/announce", - "http://bt.endpot.com:80/announce", + "udp://exodus.desync.com:6969/announce", + "udp://uploads.gamecoast.net:6969/announce", + "udp://tracker1.bt.moack.co.kr:80/announce", "udp://tracker.tiny-vps.com:6969/announce", - "udp://tracker.auctor.tv:6969/announce", - "udp://tk1.trackerservers.com:8080/announce" + "udp://tracker.theoks.net:6969/announce", + "udp://tracker.skyts.net:6969/announce", + "udp://tracker-udp.gbitt.info:80/announce", + "udp://open.tracker.ink:6969/announce", + "udp://movies.zsw.ca:6969/announce" }; public static string BuildPublicMagnetLink(string infoHash, string releaseTitle) diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index 51f7e8ae8..a9fd4ebeb 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -290,9 +290,9 @@ namespace NzbDrone.Core.Indexers Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0 }; } - catch (Exception e) + catch (Exception ex) { - _logger.Warn(e, "Failed to get enclosure for: {0}", item.Title()); + _logger.Warn(ex, "Failed to get enclosure for: {0}", item.Title()); } return null; diff --git a/src/NzbDrone.Core/Indexers/Settings/CookieTorrentBaseSettings.cs b/src/NzbDrone.Core/Indexers/Settings/CookieTorrentBaseSettings.cs index 4198550d1..6122acfa7 100644 --- a/src/NzbDrone.Core/Indexers/Settings/CookieTorrentBaseSettings.cs +++ b/src/NzbDrone.Core/Indexers/Settings/CookieTorrentBaseSettings.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Indexers.Settings Cookie = ""; } - [FieldDefinition(2, Label = "Cookie", HelpText = "Site Cookie", HelpLink = "https://wiki.servarr.com/useful-tools#finding-cookies", Privacy = PrivacyLevel.Password)] + [FieldDefinition(2, Label = "IndexerSettingsCookie", HelpText = "IndexerSettingsCookieHelpText", HelpLink = "https://wiki.servarr.com/useful-tools#finding-cookies", Privacy = PrivacyLevel.Password)] public string Cookie { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Settings/NoAuthTorrentBaseSettings.cs b/src/NzbDrone.Core/Indexers/Settings/NoAuthTorrentBaseSettings.cs index 5c9a8bea9..42f800dc8 100644 --- a/src/NzbDrone.Core/Indexers/Settings/NoAuthTorrentBaseSettings.cs +++ b/src/NzbDrone.Core/Indexers/Settings/NoAuthTorrentBaseSettings.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Indexers.Settings { private static readonly NoAuthSettingsValidator<NoAuthTorrentBaseSettings> Validator = new (); - [FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")] + [FieldDefinition(1, Label = "IndexerSettingsBaseUrl", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "IndexerSettingsBaseUrlHelpText")] public string BaseUrl { get; set; } [FieldDefinition(20)] diff --git a/src/NzbDrone.Core/Indexers/TorrentIndexerBase.cs b/src/NzbDrone.Core/Indexers/TorrentIndexerBase.cs index f727ef2f1..2dc9ae63f 100644 --- a/src/NzbDrone.Core/Indexers/TorrentIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/TorrentIndexerBase.cs @@ -1,3 +1,4 @@ +using System; using System.Text; using MonoTorrent; using NLog; @@ -22,10 +23,10 @@ namespace NzbDrone.Core.Indexers { Torrent.Load(fileData); } - catch + catch (Exception ex) { - _logger.Trace("Invalid torrent file contents: {0}", Encoding.ASCII.GetString(fileData)); - throw; + _logger.Debug("Invalid torrent file contents: {0}", Encoding.ASCII.GetString(fileData)); + throw new NotSupportedException($"Invalid torrent file contents. Reason: {ex.Message}", ex); } } } diff --git a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs index e33cebc4d..1c334a19c 100644 --- a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs +++ b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs @@ -60,44 +60,46 @@ namespace NzbDrone.Core.Instrumentation { try { - var log = new Log(); - log.Time = logEvent.TimeStamp; - log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage); - - log.Logger = logEvent.LoggerName; + var log = new Log + { + Time = logEvent.TimeStamp, + Logger = logEvent.LoggerName, + Level = logEvent.Level.Name + }; if (log.Logger.StartsWith("NzbDrone.")) { log.Logger = log.Logger.Remove(0, 9); } + var message = logEvent.FormattedMessage; + if (logEvent.Exception != null) { - if (string.IsNullOrWhiteSpace(log.Message)) + if (string.IsNullOrWhiteSpace(message)) { - log.Message = logEvent.Exception.Message; + message = logEvent.Exception.Message; } else { - log.Message += ": " + logEvent.Exception.Message; + message += ": " + logEvent.Exception.Message; } - log.Exception = logEvent.Exception.ToString(); + log.Exception = CleanseLogMessage.Cleanse(logEvent.Exception.ToString()); log.ExceptionType = logEvent.Exception.GetType().ToString(); } - log.Level = logEvent.Level.Name; + log.Message = CleanseLogMessage.Cleanse(message); - var connectionString = _connectionStringFactory.LogDbConnectionString; + var connectionInfo = _connectionStringFactory.LogDbConnection; - //TODO: Probably need more robust way to differentiate what's being used - if (connectionString.Contains(".db")) + if (connectionInfo.DatabaseType == DatabaseType.SQLite) { - WriteSqliteLog(log, connectionString); + WriteSqliteLog(log, connectionInfo.ConnectionString); } else { - WritePostgresLog(log, connectionString); + WritePostgresLog(log, connectionInfo.ConnectionString); } } catch (NpgsqlException ex) @@ -136,8 +138,10 @@ namespace NzbDrone.Core.Instrumentation private void WriteSqliteLog(Log log, string connectionString) { using (var connection = - new SQLiteConnection(connectionString).OpenAndReturn()) + SQLiteFactory.Instance.CreateConnection()) { + connection.ConnectionString = connectionString; + connection.Open(); using (var sqlCommand = connection.CreateCommand()) { sqlCommand.CommandText = INSERT_COMMAND; diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index 38b695ccc..ac0cd085b 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NLog.Config; +using NLog.Targets; using NLog.Targets.Syslog; using NLog.Targets.Syslog.Settings; using NzbDrone.Common.EnvironmentInfo; @@ -50,14 +51,15 @@ namespace NzbDrone.Core.Instrumentation var rules = LogManager.Configuration.LoggingRules; - //Console + // Console + ReconfigureConsole(); SetMinimumLogLevel(rules, "consoleLogger", minimumConsoleLogLevel); //Log Files SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off); SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off); SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off); - SetLogRotation(); + ReconfigureFile(); //Log Sql SqlBuilderExtensions.LogSql = _configFileProvider.LogSql; @@ -91,11 +93,12 @@ namespace NzbDrone.Core.Instrumentation } } - private void SetLogRotation() + private void ReconfigureFile() { - foreach (var target in LogManager.Configuration.AllTargets.OfType<NzbDroneFileTarget>()) + foreach (var target in LogManager.Configuration.AllTargets.OfType<CleansingFileTarget>()) { target.MaxArchiveFiles = _configFileProvider.LogRotate; + target.ArchiveAboveSize = _configFileProvider.LogSizeLimit.Megabytes(); } } @@ -109,6 +112,18 @@ namespace NzbDrone.Core.Instrumentation } } + private void ReconfigureConsole() + { + var consoleTarget = LogManager.Configuration.AllTargets.OfType<ColoredConsoleTarget>().FirstOrDefault(); + + if (consoleTarget != null) + { + var format = _configFileProvider.ConsoleLogFormat; + + NzbDroneLogger.ConfigureConsoleLayout(consoleTarget, format); + } + } + private void SetSyslogParameters(string syslogServer, int syslogPort, LogLevel minimumLogLevel) { var syslogTarget = new SyslogTarget(); @@ -117,6 +132,7 @@ namespace NzbDrone.Core.Instrumentation syslogTarget.MessageSend.Protocol = ProtocolType.Udp; syslogTarget.MessageSend.Udp.Port = syslogPort; syslogTarget.MessageSend.Udp.Server = syslogServer; + syslogTarget.MessageSend.Retry.ConstantBackoff.BaseDelay = 500; syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424; syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName; diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index f4b081b30..c6cff60ad 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Jobs new ScheduledTask { Interval = 6 * 60, - TypeName = typeof(ApplicationCheckUpdateCommand).FullName + TypeName = typeof(ApplicationUpdateCheckCommand).FullName }, new ScheduledTask diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index 1f6073725..3cd2a3687 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -29,7 +29,7 @@ "DeleteDownloadClientMessageText": "هل أنت متأكد أنك تريد حذف برنامج التنزيل \"{0}\"؟", "Filename": "اسم الملف", "HomePage": "الصفحة الرئيسية", - "IndexerProxyStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {indexerProxyNames}", "Language": "لغة", "Usenet": "يوزنت", "MovieIndexScrollTop": "فهرس الفيلم: قم بالتمرير لأعلى", @@ -47,8 +47,8 @@ "Protocol": "بروتوكول", "Analytics": "تحليلات", "ProxyBypassFilterHelpText": "استخدم \"،\" كفاصل ، و \"*.\" كحرف بدل للنطاقات الفرعية", - "ProxyCheckBadRequestMessage": "فشل اختبار الوكيل. رمز الحالة: {0}", - "ProxyCheckFailedToTestMessage": "فشل اختبار الوكيل: {0}", + "ProxyBadRequestHealthCheckMessage": "فشل اختبار الوكيل. رمز الحالة: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "فشل اختبار الوكيل: {url}", "ProxyPasswordHelpText": "ما عليك سوى إدخال اسم مستخدم وكلمة مرور إذا كان أحدهما مطلوبًا. اتركها فارغة وإلا.", "ProxyUsernameHelpText": "ما عليك سوى إدخال اسم مستخدم وكلمة مرور إذا كان أحدهما مطلوبًا. اتركها فارغة وإلا.", "Queue": "طابور", @@ -59,16 +59,16 @@ "SelectAll": "اختر الكل", "SendAnonymousUsageData": "إرسال بيانات الاستخدام المجهولة", "Style": "أسلوب", - "SystemTimeCheckMessage": "توقف وقت النظام بأكثر من يوم واحد. قد لا تعمل المهام المجدولة بشكل صحيح حتى يتم تصحيح الوقت", + "SystemTimeHealthCheckMessage": "توقف وقت النظام بأكثر من يوم واحد. قد لا تعمل المهام المجدولة بشكل صحيح حتى يتم تصحيح الوقت", "TableOptionsColumnsMessage": "اختر الأعمدة المرئية والترتيب الذي تظهر به", "TagCannotBeDeletedWhileInUse": "لا يمكن حذفه أثناء الاستخدام", "Tasks": "مهام", "UnableToAddANewAppProfilePleaseTryAgain": "غير قادر على إضافة ملف تعريف جودة جديد ، يرجى المحاولة مرة أخرى.", "UnableToAddANewDownloadClientPleaseTryAgain": "غير قادر على إضافة عميل تنزيل جديد ، يرجى المحاولة مرة أخرى.", "UnableToAddANewIndexerPleaseTryAgain": "غير قادر على إضافة مفهرس جديد ، يرجى المحاولة مرة أخرى.", - "UnableToLoadBackups": "تعذر تحميل النسخ الاحتياطية", + "BackupsLoadError": "تعذر تحميل النسخ الاحتياطية", "UnsavedChanges": "التغييرات غير المحفوظة", - "UpdateCheckUINotWritableMessage": "لا يمكن تثبيت التحديث لأن مجلد واجهة المستخدم '{0}' غير قابل للكتابة بواسطة المستخدم '{1}'", + "UpdateUiNotWritableHealthCheckMessage": "لا يمكن تثبيت التحديث لأن مجلد واجهة المستخدم '{uiFolder}' غير قابل للكتابة بواسطة المستخدم '{userName}'", "UpdateScriptPathHelpText": "المسار إلى برنامج نصي مخصص يأخذ حزمة تحديث مستخرجة ويتعامل مع ما تبقى من عملية التحديث", "Username": "اسم المستخدم", "Warn": "حذر", @@ -76,19 +76,19 @@ "EnableInteractiveSearch": "تمكين البحث التفاعلي", "Source": "مصدر", "SSLCertPassword": "كلمة مرور شهادة SSL", - "UpdateCheckStartupNotWritableMessage": "لا يمكن تثبيت التحديث لأن مجلد بدء التشغيل \"{0}\" غير قابل للكتابة بواسطة المستخدم \"{1}\".", + "UpdateStartupNotWritableHealthCheckMessage": "لا يمكن تثبيت التحديث لأن مجلد بدء التشغيل \"{startupFolder}\" غير قابل للكتابة بواسطة المستخدم \"{userName}\".", "UpdateMechanismHelpText": "استخدم المحدث أو البرنامج النصي المدمج في {appName}", "AppDataDirectory": "دليل AppData", "ConnectSettings": "ربط الإعدادات", "CouldNotConnectSignalR": "تعذر الاتصال بـ SignalR ، لن يتم تحديث واجهة المستخدم", "Dates": "تواريخ", - "DBMigration": "ترحيل DB", + "DatabaseMigration": "ترحيل DB", "Delete": "حذف", "Details": "تفاصيل", "Donations": "التبرعات", "DownloadClient": "تحميل العميل", - "IndexerStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل", - "IndexerStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "جميع المفهرسات غير متوفرة بسبب الفشل", + "IndexerStatusUnavailableHealthCheckMessage": "المفهرسات غير متاحة بسبب الإخفاقات: {indexerNames}", "Info": "معلومات", "Interval": "فترة", "Manual": "كتيب", @@ -124,16 +124,16 @@ "FocusSearchBox": "التركيز على مربع البحث", "Folder": "مجلد", "Health": "الصحة", - "HealthNoIssues": "لا مشاكل مع التكوين الخاص بك", + "NoIssuesWithYourConfiguration": "لا مشاكل مع التكوين الخاص بك", "Host": "مضيف", "IllRestartLater": "سأعيد التشغيل لاحقًا", "IncludeHealthWarningsHelpText": "قم بتضمين التحذيرات الصحية", "Indexer": "مفهرس", "IndexerFlags": "أعلام المفهرس", - "IndexerLongTermStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل لأكثر من 6 ساعات", - "IndexerLongTermStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات لأكثر من 6 ساعات: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "جميع المفهرسات غير متوفرة بسبب الفشل لأكثر من 6 ساعات", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "المفهرسات غير متاحة بسبب الإخفاقات لأكثر من 6 ساعات: {indexerNames}", "IndexerPriorityHelpText": "أولوية المفهرس من 1 (الأعلى) إلى 50 (الأدنى). الافتراضي: 25.", - "IndexerProxyStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "جميع المفهرسات غير متوفرة بسبب الفشل", "NoChange": "لا تغيير", "NoLogFiles": "لا توجد ملفات سجل", "NoTagsHaveBeenAddedYet": "لم يتم إضافة أي علامات حتى الان", @@ -150,7 +150,7 @@ "RemovingTag": "إزالة العلامة", "Reset": "إعادة تعيين", "ResetAPIKey": "إعادة تعيين مفتاح API", - "RSSIsNotSupportedWithThisIndexer": "لا يتم دعم RSS مع هذا المفهرس", + "RssIsNotSupportedWithThisIndexer": "لا يتم دعم RSS مع هذا المفهرس", "Save": "حفظ", "ScriptPath": "مسار البرنامج النصي", "SettingsEnableColorImpairedMode": "تفعيل وضع ضعف الألوان", @@ -185,7 +185,7 @@ "NetCore": ".شبكة", "Password": "كلمه السر", "Proxy": "الوكيل", - "ProxyCheckResolveIpMessage": "فشل حل عنوان IP لمضيف الخادم الوكيل المكون {0}", + "ProxyResolveIpHealthCheckMessage": "فشل حل عنوان IP لمضيف الخادم الوكيل المكون {proxyHostName}", "ProxyType": "نوع الوكيل", "ReleaseStatus": "حالة الإصدار", "Search": "بحث", @@ -212,8 +212,8 @@ "DeleteIndexerProxyMessageText": "هل أنت متأكد أنك تريد حذف العلامة \"{0}\"؟", "DeleteNotification": "حذف الإعلام", "DownloadClients": "تحميل العملاء", - "DownloadClientStatusCheckAllClientMessage": "جميع عملاء التنزيل غير متاحين بسبب الفشل", - "DownloadClientStatusCheckSingleClientMessage": "برامج التنزيل غير متاحة بسبب الإخفاقات: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "جميع عملاء التنزيل غير متاحين بسبب الفشل", + "DownloadClientStatusSingleClientHealthCheckMessage": "برامج التنزيل غير متاحة بسبب الإخفاقات: {downloadClientNames}", "Edit": "تعديل", "EditIndexer": "تحرير المفهرس", "Enable": "ممكن", @@ -251,16 +251,16 @@ "NoChanges": "لا تغيرات", "NoLeaveIt": "لا ، اتركها", "PendingChangesDiscardChanges": "تجاهل التغييرات واترك", - "RSS": "RSS", + "Rss": "RSS", "System": "النظام", "TagIsNotUsedAndCanBeDeleted": "العلامة غير مستخدمة ويمكن حذفها", "Tags": "العلامات", "TagsHelpText": "ينطبق على الأفلام التي تحتوي على علامة مطابقة واحدة على الأقل", "UISettings": "إعدادات واجهة المستخدم", - "UnableToLoadDownloadClients": "تعذر تحميل عملاء التنزيل", + "DownloadClientsLoadError": "تعذر تحميل عملاء التنزيل", "UnableToLoadTags": "تعذر تحميل العلامات", "UnableToLoadUISettings": "تعذر تحميل إعدادات واجهة المستخدم", - "UpdateCheckStartupTranslocationMessage": "لا يمكن تثبيت التحديث لأن مجلد بدء التشغيل \"{0}\" موجود في مجلد App Translocation.", + "UpdateStartupTranslocationHealthCheckMessage": "لا يمكن تثبيت التحديث لأن مجلد بدء التشغيل \"{startupFolder}\" موجود في مجلد App Translocation.", "Updates": "التحديثات", "Version": "الإصدار", "ApiKey": "مفتاح API", @@ -329,7 +329,7 @@ "Queued": "في قائمة الانتظار", "Remove": "إزالة", "Replace": "يحل محل", - "TheLatestVersionIsAlreadyInstalled": "تم بالفعل تثبيت أحدث إصدار من {0}", + "OnLatestVersion": "تم بالفعل تثبيت أحدث إصدار من {0}", "DownloadClientPriorityHelpText": "تحديد أولويات عملاء التنزيل المتعددين. يتم استخدام Round-Robin للعملاء الذين لديهم نفس الأولوية.", "ApplyTagsHelpTextAdd": "إضافة: أضف العلامات إلى قائمة العلامات الموجودة", "ApplyTagsHelpTextHowToApplyApplications": "كيفية تطبيق العلامات على الأفلام المختارة", @@ -341,17 +341,39 @@ "More": "أكثر", "Track": "أثر", "Year": "عام", - "ConnectionLostReconnect": "سيحاول Radarr الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه.", + "ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه.", "DeleteAppProfileMessageText": "هل أنت متأكد من أنك تريد حذف ملف تعريف الجودة {0}", "RecentChanges": "التغييرات الأخيرة", "WhatsNew": "ما هو الجديد؟", "minutes": "الدقائق", "NotificationStatusAllClientHealthCheckMessage": "جميع القوائم غير متاحة بسبب الإخفاقات", - "NotificationStatusSingleClientHealthCheckMessage": "القوائم غير متاحة بسبب الإخفاقات: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "القوائم غير متاحة بسبب الإخفاقات: {notificationNames}", "AuthBasic": "أساسي (المتصفح المنبثق)", "AuthForm": "النماذج (صفحة تسجيل الدخول)", "DisabledForLocalAddresses": "معطل بسبب العناوين المحلية", "None": "لا شيء", "ResetAPIKeyMessageText": "هل أنت متأكد أنك تريد إعادة تعيين مفتاح API الخاص بك؟", - "RestartProwlarr": "أعد تشغيل {appName}" + "RestartProwlarr": "أعد تشغيل {appName}", + "CustomFilter": "مرشحات مخصصة", + "IndexerHDBitsSettingsMediums": "متوسط", + "GrabRelease": "انتزاع الإصدار", + "ProxyValidationBadRequest": "فشل اختبار الوكيل. رمز الحالة: {statusCode}", + "Script": "النصي", + "BuiltIn": "مدمج", + "PublishedDate": "تاريخ النشر", + "AllSearchResultsHiddenByFilter": "يتم إخفاء جميع النتائج بواسطة عامل التصفية المطبق", + "NoEventsFound": "لم يتم العثور على أحداث", + "RestartReloadNote": "ملاحظة: سيتم إعادة تشغيل {appName} تلقائيًا وإعادة تحميل واجهة المستخدم أثناء عملية الاستعادة.", + "UpdateAppDirectlyLoadError": "تعذر تحديث {appName} مباشرة ،", + "DockerUpdater": "تحديث حاوية عامل الإرساء لتلقي التحديث", + "Download": "تحميل", + "ErrorRestoringBackup": "خطأ في استعادة النسخة الاحتياطية", + "ExternalUpdater": "تم تكوين {appName} لاستخدام آلية تحديث خارجية", + "InstallLatest": "تثبيت الأحدث", + "AptUpdater": "استخدم apt لتثبيت التحديث", + "Clone": "قريب", + "Stats": "الحالة", + "CurrentlyInstalled": "مثبتة حاليا", + "Season": "السبب", + "Mixed": "ثابت" } diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index acab726cb..913275a68 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -35,7 +35,7 @@ "CustomFilters": "Персонализирани филтри", "Date": "Дата", "Dates": "Дати", - "DBMigration": "DB миграция", + "DatabaseMigration": "DB миграция", "Delete": "Изтрий", "DeleteDownloadClient": "Изтриване на клиент за изтегляне", "DeleteDownloadClientMessageText": "Наистина ли искате да изтриете клиента за изтегляне '{0}'?", @@ -61,11 +61,11 @@ "DeleteBackupMessageText": "Наистина ли искате да изтриете резервното копие '{0}'?", "DeleteTag": "Изтриване на маркера", "DeleteTagMessageText": "Наистина ли искате да изтриете маркера '{0}'?", - "IndexerProxyStatusCheckAllClientMessage": "Всички списъци са недостъпни поради неуспехи", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Всички списъци са недостъпни поради неуспехи", "LastWriteTime": "Време за последно писане", "Columns": "Колони", "EnableRss": "Активиране на RSS", - "IndexerProxyStatusCheckSingleClientMessage": "Списъци, недостъпни поради неуспехи: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Списъци, недостъпни поради неуспехи: {indexerProxyNames}", "LogLevel": "Log Level", "MovieIndexScrollTop": "Индекс на филма: Превъртете отгоре", "Queue": "Опашка", @@ -90,7 +90,7 @@ "SSLPort": "SSL порт", "Status": "Състояние", "System": "Система", - "SystemTimeCheckMessage": "Системното време е изключено с повече от 1 ден. Планираните задачи може да не се изпълняват правилно, докато времето не бъде коригирано", + "SystemTimeHealthCheckMessage": "Системното време е изключено с повече от 1 ден. Планираните задачи може да не се изпълняват правилно, докато времето не бъде коригирано", "TestAll": "Тествайте всички", "Title": "Заглавие", "Today": "Днес", @@ -102,7 +102,7 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "Не може да се добави нов индексатор, моля, опитайте отново.", "UnableToAddANewNotificationPleaseTryAgain": "Не може да се добави ново известие, моля, опитайте отново.", "UpdateAutomaticallyHelpText": "Автоматично изтегляне и инсталиране на актуализации. Все още ще можете да инсталирате от System: Updates", - "UpdateCheckStartupTranslocationMessage": "Не може да се инсталира актуализация, защото стартовата папка „{0}“ е в папка за преместване на приложения.", + "UpdateStartupTranslocationHealthCheckMessage": "Не може да се инсталира актуализация, защото стартовата папка „{startupFolder}“ е в папка за преместване на приложения.", "ReleaseStatus": "Състояние на освобождаване", "UpdateScriptPathHelpText": "Път към персонализиран скрипт, който взема извлечен пакет за актуализация и обработва останалата част от процеса на актуализация", "UseProxy": "Използвай прокси", @@ -120,7 +120,7 @@ "Grabbed": "Грабната", "Grabs": "Грабнете", "Health": "Здраве", - "HealthNoIssues": "Няма проблеми с вашата конфигурация", + "NoIssuesWithYourConfiguration": "Няма проблеми с вашата конфигурация", "HomePage": "Начална страница", "Hostname": "Име на хост", "IgnoredAddresses": "Игнорирани адреси", @@ -153,7 +153,7 @@ "UISettings": "Настройки на потребителския интерфейс", "UnableToAddANewApplicationPleaseTryAgain": "Не може да се добави ново известие, моля, опитайте отново.", "UnableToAddANewAppProfilePleaseTryAgain": "Не може да се добави нов качествен профил, моля, опитайте отново.", - "UnableToLoadBackups": "Архивите не могат да се заредят", + "BackupsLoadError": "Архивите не могат да се заредят", "AllIndexersHiddenDueToFilter": "Всички филми са скрити поради приложен филтър.", "Level": "Ниво", "ApplicationStatusCheckAllClientMessage": "Всички списъци са недостъпни поради неуспехи", @@ -170,7 +170,7 @@ "RemovingTag": "Премахване на етикет", "Reset": "Нулиране", "Retention": "Задържане", - "RSS": "RSS", + "Rss": "RSS", "Warn": "Предупреждавайте", "NoTagsHaveBeenAddedYet": "Все още не са добавени етикети", "NotificationTriggers": "Задействания за уведомяване", @@ -188,7 +188,7 @@ "Priority": "Приоритет", "SettingsLongDateFormat": "Формат с дълга дата", "SettingsShowRelativeDates": "Показване на относителни дати", - "UnableToLoadDownloadClients": "Клиентите за изтегляне не могат да се заредят", + "DownloadClientsLoadError": "Клиентите за изтегляне не могат да се заредят", "Logging": "Регистрация", "Exception": "Изключение", "MovieIndexScrollBottom": "Индекс на филма: Превъртане отдолу", @@ -200,9 +200,9 @@ "PortNumber": "Номер на пристанище", "Proxy": "Прокси", "ProxyBypassFilterHelpText": "Използвайте „,“ като разделител и „*“. като заместващ знак за поддомейни", - "ProxyCheckBadRequestMessage": "Неуспешно тестване на прокси. Код на състоянието: {0}", - "ProxyCheckFailedToTestMessage": "Неуспешно тестване на прокси: {0}", - "ProxyCheckResolveIpMessage": "Неуспешно разрешаване на IP адреса за конфигурирания прокси хост {0}", + "ProxyBadRequestHealthCheckMessage": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Неуспешно тестване на прокси: {url}", + "ProxyResolveIpHealthCheckMessage": "Неуспешно разрешаване на IP адреса за конфигурирания прокси хост {proxyHostName}", "ProxyPasswordHelpText": "Трябва само да въведете потребителско име и парола, ако е необходимо. В противен случай ги оставете празни.", "ProxyType": "Тип прокси", "ExistingTag": "Съществуващ маркер", @@ -239,8 +239,8 @@ "Test": "Тест", "UI": "Потребителски интерфейс", "UILanguage": "Език на потребителския интерфейс", - "UpdateCheckStartupNotWritableMessage": "Не може да се инсталира актуализация, тъй като стартовата папка „{0}“ не може да се записва от потребителя „{1}“.", - "UpdateCheckUINotWritableMessage": "Не може да се инсталира актуализация, защото папката „{0}“ на потребителския интерфейс не може да се записва от потребителя „{1}“.", + "UpdateStartupNotWritableHealthCheckMessage": "Не може да се инсталира актуализация, тъй като стартовата папка „{startupFolder}“ не може да се записва от потребителя „{userName}“.", + "UpdateUiNotWritableHealthCheckMessage": "Не може да се инсталира актуализация, защото папката „{uiFolder}“ на потребителския интерфейс не може да се записва от потребителя „{userName}“.", "Enable": "Активиране", "EventType": "Тип на събитието", "Failed": "Се провали", @@ -253,13 +253,13 @@ "HideAdvanced": "Скрий Разширено", "Indexer": "Индексатор", "IndexerFlags": "Индексиращи знамена", - "IndexerLongTermStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки за повече от 6 часа", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Всички индексатори са недостъпни поради грешки за повече от 6 часа", "SSLCertPathHelpText": "Път към pfx файл", "UrlBaseHelpText": "За обратна поддръжка на прокси по подразбиране е празно", "View": "Изглед", "BranchUpdate": "Клон, който да се използва за актуализиране на {appName}", "Indexers": "Индексатори", - "IndexerStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки", + "IndexerStatusAllUnavailableHealthCheckMessage": "Всички индексатори са недостъпни поради грешки", "Mode": "Режим", "TagsSettingsSummary": "Вижте всички тагове и как се използват. Неизползваните маркери могат да бъдат премахнати", "TestAllClients": "Тествайте всички клиенти", @@ -272,8 +272,8 @@ "Updates": "Актуализации", "Uptime": "Време за работа", "DownloadClientSettings": "Изтеглете настройките на клиента", - "DownloadClientStatusCheckAllClientMessage": "Всички клиенти за изтегляне са недостъпни поради неуспехи", - "DownloadClientStatusCheckSingleClientMessage": "Клиентите за изтегляне са недостъпни поради грешки: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "Всички клиенти за изтегляне са недостъпни поради неуспехи", + "DownloadClientStatusSingleClientHealthCheckMessage": "Клиентите за изтегляне са недостъпни поради грешки: {downloadClientNames}", "Edit": "редактиране", "EnableAutomaticSearch": "Активирайте автоматичното търсене", "EnableAutomaticSearchHelpText": "Ще се използва, когато се извършват автоматични търсения чрез потребителския интерфейс или от {appName}", @@ -288,15 +288,15 @@ "GeneralSettingsSummary": "Порт, SSL, потребителско име / парола, прокси, анализи и актуализации", "History": "История", "Host": "Водещ", - "IndexerLongTermStatusCheckSingleClientMessage": "Индексатори не са налични поради неуспехи за повече от 6 часа: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Индексатори не са налични поради неуспехи за повече от 6 часа: {indexerNames}", "IndexerPriorityHelpText": "Приоритет на индексатора от 1 (най-висок) до 50 (най-нисък). По подразбиране: 25.", - "IndexerStatusCheckSingleClientMessage": "Индексатори не са налични поради грешки: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Индексатори не са налични поради грешки: {indexerNames}", "LaunchBrowserHelpText": " Отворете уеб браузър и отворете началната страница на {appName} при стартиране на приложението.", "ResetAPIKey": "Нулиране на API ключ", "Restart": "Рестартирам", "RestartNow": "Рестартирай сега", "Restore": "Възстанови", - "RSSIsNotSupportedWithThisIndexer": "RSS не се поддържа с този индексатор", + "RssIsNotSupportedWithThisIndexer": "RSS не се поддържа с този индексатор", "Wiki": "Wiki", "PageSizeHelpText": "Брой елементи за показване на всяка страница", "Password": "Парола", @@ -329,7 +329,7 @@ "Queued": "На опашка", "Remove": "Премахване", "Replace": "Сменете", - "TheLatestVersionIsAlreadyInstalled": "Вече е инсталирана най-новата версия на {0}", + "OnLatestVersion": "Вече е инсталирана най-новата версия на {0}", "Genre": "Жанрове", "ApplyTagsHelpTextRemove": "Премахване: Премахнете въведените тагове", "ApplyTagsHelpTextHowToApplyIndexers": "Как да приложите тагове към избраните филми", @@ -346,12 +346,76 @@ "RecentChanges": "Последни промени", "WhatsNew": "Какво ново?", "minutes": "Минути", - "NotificationStatusSingleClientHealthCheckMessage": "Списъци, недостъпни поради неуспехи: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Списъци, недостъпни поради неуспехи: {notificationNames}", "NotificationStatusAllClientHealthCheckMessage": "Всички списъци са недостъпни поради неуспехи", "AuthBasic": "Основно (изскачащ прозорец на браузъра)", "AuthForm": "Формуляри (Страница за вход)", "DisabledForLocalAddresses": "Забранено за местни адреси", "None": "Нито един", "ResetAPIKeyMessageText": "Наистина ли искате да нулирате своя API ключ?", - "RestartProwlarr": "Рестартирайте {appName}" + "RestartProwlarr": "Рестартирайте {appName}", + "IndexerHDBitsSettingsMediums": "Среден", + "CustomFilter": "Персонализирани филтри", + "GrabRelease": "Grab Release", + "ProxyValidationBadRequest": "Неуспешно тестване на прокси. Код на състоянието: {statusCode}", + "BuiltIn": "Вграден", + "Script": "Сценарий", + "PublishedDate": "Дата на публикуване", + "AllSearchResultsHiddenByFilter": "Всички резултати са скрити от приложения филтър", + "DockerUpdater": "актуализирайте контейнера на докера, за да получите актуализацията", + "Download": "Изтегли", + "ErrorRestoringBackup": "Грешка при възстановяване на архивиране", + "ExternalUpdater": "{appName} е конфигуриран да използва външен механизъм за актуализация", + "NoEventsFound": "Няма намерени събития", + "RestartReloadNote": "Забележка: {appName} автоматично ще рестартира и презареди потребителския интерфейс по време на процеса на възстановяване.", + "UpdateAppDirectlyLoadError": "Не може да се актуализира {appName} директно,", + "AptUpdater": "Използвайте apt, за да инсталирате актуализацията", + "InstallLatest": "Инсталирайте най-новите", + "Clone": "Близо", + "ActiveApps": "Активни приложения", + "ActiveIndexers": "Активни индиксатори", + "AddApplication": "добави приложение", + "Season": "Причина", + "CurrentlyInstalled": "Понастоящем инсталиран", + "DownloadClientSettingsAddPaused": "Добави на пауза", + "Encoding": "Кодиране", + "Episode": "епизод", + "Applications": "Приложения", + "Publisher": "Издател", + "Id": "ИН", + "Theme": "Тема", + "Label": "Етикет", + "Categories": "Категории", + "Album": "албум", + "Artist": "изпълнител", + "AddConnection": "Добави връзка", + "AddConnectionImplementation": "Добави връзка - {implementationName}", + "AddDownloadClientImplementation": "Добави клиент за изтегляне - {implementationName}", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Потвърдете новата парола", + "Default": "Подразбиране", + "Any": "Всеки", + "ApplicationUrlHelpText": "Външният URL адрес на това приложение, включително http(s)://, порт и основно URL", + "Database": "База данни", + "Destination": "Дестинация", + "DownloadClientAriaSettingsDirectoryHelpText": "Незадължително локация за изтеглянията, оставете празно, за да използвате локацията по подразбиране на Aria2", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Добавя префикс към url адреса на deluge json, вижте {url}", + "Directory": "Директория", + "AddIndexerImplementation": "Добави индексатор - {implementationName}", + "AuthenticationRequiredHelpText": "Променете за кои заявки се изисква удостоверяване. Не променяйте, освен ако не разбирате рисковете.", + "AuthenticationRequiredPasswordHelpTextWarning": "Въведете нова парола", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Незадължителна споделена папка, в която да се поставят изтеглянията, оставете празно, за да използвате местоположението по подразбиране на Download Station", + "DownloadClientFloodSettingsAdditionalTags": "Допълнителни тагове", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавя свойствата на медията като тагове. Напътствията са примери.", + "DownloadClientFloodSettingsTagsHelpText": "Първоначални тагове на изтегляне. За да бъде разпознато едно изтегляне, то трябва да има всички начални тагове. По този начин се избягват конфликти с необвързани с приложение изтегляния.", + "ApplicationURL": "URL адрес на приложението", + "AuthenticationRequired": "Изисква се удостоверяване", + "ApplyChanges": "Прилагане на промените", + "ApiKeyValidationHealthCheckMessage": "Моля, актуализирайте API ключа си така, че да съдържа поне {length} знака. Можете да направите това чрез настройките или конфигурационния файл", + "AppUpdated": "{appName} Актуализиран", + "AppUpdatedVersion": "{appName} е актуализиранa до версия `{version}`, за да получите най-новите промени, ще трябва да презаредите {appName}", + "Donate": "Дарете", + "AddCustomFilter": "Добави персонализиран филтър", + "AuthenticationMethod": "Метод за удостоверяване", + "AuthenticationMethodHelpTextWarning": "Моля, изберете валиден метод за удостоверяване", + "BlackholeFolderHelpText": "Папка, в която {appName} ще съхранява файла {extension}" } diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index c3c962957..83a720b51 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -1,5 +1,5 @@ { - "Add": "Afegiu", + "Add": "Afegeix", "Actions": "Accions", "AcceptConfirmationModal": "Accepta el modal de confirmació", "About": "Quant a", @@ -19,7 +19,7 @@ "ScriptPath": "Camí de l'script", "Search": "Cerca", "Files": "Fitxers", - "SettingsEnableColorImpairedModeHelpText": "Estil alternat per permetre als usuaris amb problemes de color distingir millor la informació codificada per colors", + "SettingsEnableColorImpairedModeHelpText": "Estil modificat per a permetre als usuaris amb problemes visuals distingir millor la informació codificada per colors", "TagIsNotUsedAndCanBeDeleted": "L'etiqueta no està en ús i es pot suprimir", "TagsSettingsSummary": "Consulta totes les etiquetes i com s'utilitzen. Les etiquetes no utilitzades es poden eliminar", "Tasks": "Tasques", @@ -33,18 +33,18 @@ "AddDownloadClient": "Afegeix un client de descàrrega", "Added": "Afegit", "Age": "Edat", - "All": "Tots", + "All": "Tot", "Analytics": "Anàlisi", "ApiKey": "Clau API", "AppDataDirectory": "Directori AppData", - "AppDataLocationHealthCheckMessage": "No es podrà actualitzar per evitar que s'eliminin AppData a l'actualització", + "AppDataLocationHealthCheckMessage": "No es podrà actualitzar per a evitar que s'eliminin AppData a l'actualització", "Authentication": "Autenticació", "Torrents": "Torrents", "Type": "Tipus", "UILanguageHelpTextWarning": "Es requereix una recàrrega del navegador", "UISettings": "Configuració de la interfície", - "UnableToLoadBackups": "No es poden carregar còpies de seguretat", - "UnableToLoadDownloadClients": "No es poden carregar els clients de baixada", + "BackupsLoadError": "No es poden carregar còpies de seguretat", + "DownloadClientsLoadError": "No es poden carregar els clients de baixada", "UnableToLoadTags": "No es poden carregar les etiquetes", "UnableToLoadUISettings": "No es pot carregar la configuració de la IU", "UnselectAll": "Desseleccioneu-ho tot", @@ -74,15 +74,15 @@ "Retention": "Retenció", "Title": "Títol", "DeleteNotification": "Suprimeix la notificació", - "ProxyCheckBadRequestMessage": "No s'ha pogut provar el servidor intermediari. Codi d'estat: {0}", + "ProxyBadRequestHealthCheckMessage": "No s'ha pogut provar el servidor intermediari. Codi d'estat: {statusCode}", "Reddit": "Reddit", "System": "Sistema", "Username": "Nom d'usuari", - "Duration": "durada", + "Duration": "Durada", "EditIndexer": "Edita l'indexador", "EnableAutomaticSearch": "Activa la cerca automàtica", "Enabled": "Habilitat", - "Error": "error", + "Error": "Error", "ErrorLoadingContents": "S'ha produït un error en carregar el contingut", "Events": "Esdeveniments", "ExistingTag": "Etiqueta existent", @@ -128,22 +128,22 @@ "Filters": "Filtres", "FocusSearchBox": "Posa el focus a la caixa de cerca", "Grabbed": "Capturat", - "IndexerStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors", - "IndexerStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tots els indexadors no estan disponibles a causa d'errors", + "IndexerStatusUnavailableHealthCheckMessage": "Els indexadors no estan disponibles a causa d'errors: {indexerNames}", "MovieIndexScrollTop": "Índex de pel·lícules: Desplaçament superior", "NotificationTriggers": "Activadors de notificacions", "NotificationTriggersHelpText": "Seleccioneu quins esdeveniments haurien d'activar aquesta notificació", "Priority": "Prioritat", "SendAnonymousUsageData": "Envia dades d'ús anònimes", "SetTags": "Estableix etiquetes", - "SystemTimeCheckMessage": "L'hora del sistema està apagada durant més d'1 dia. És possible que les tasques programades no s'executin correctament fins que no es corregeixi l'hora", + "SystemTimeHealthCheckMessage": "L'hora del sistema està apagada durant més d'1 dia. És possible que les tasques programades no s'executin correctament fins que no es corregeixi l'hora", "TableOptions": "Opcions de taula", "TableOptionsColumnsMessage": "Trieu quines columnes són visibles i en quin ordre apareixen", "Columns": "Columnes", "SettingsShortDateFormat": "Format de data curta", "BindAddress": "Adreça d'enllaç", "Database": "Base de dades", - "Ended": "S'ha acabat", + "Ended": "Acabat", "SettingsTimeFormat": "Format horari", "YesCancel": "Si, cancel·la", "Automatic": "Automàtic", @@ -168,14 +168,14 @@ "CustomFilters": "Filtres personalitzats", "Date": "Data", "Dates": "Dates", - "DBMigration": "Migració de BD", + "DatabaseMigration": "Migració de BD", "Delete": "Suprimeix", - "DeleteNotificationMessageText": "Esteu segur que voleu suprimir la notificació '{0}'?", + "DeleteNotificationMessageText": "Esteu segur que voleu suprimir la notificació '{name}'?", "DeleteTag": "Suprimeix l'etiqueta", - "DeleteTagMessageText": "Esteu segur que voleu suprimir l'etiqueta '{0}'?", + "DeleteTagMessageText": "Esteu segur que voleu suprimir l'etiqueta '{label}'?", "Details": "Detalls", "Disabled": "Desactivat", - "DownloadClientStatusCheckAllClientMessage": "Tots els clients de descàrrega no estan disponibles a causa d'errors", + "DownloadClientStatusAllClientHealthCheckMessage": "Tots els clients de descàrrega no estan disponibles a causa d'errors", "Edit": "Edita", "EnableInteractiveSearchHelpText": "S'utilitzarà quan s'utilitzi la cerca interactiva", "EnableInteractiveSearch": "Activa la cerca interactiva", @@ -196,16 +196,16 @@ "MovieIndexScrollBottom": "Índex de pel·lícules: Desplaçament inferior", "OnApplicationUpdate": "A l'actualitzar de l'aplicació", "OnApplicationUpdateHelpText": "A l'actualitzar de l'aplicació", - "OnGrab": "Al capturar", + "OnGrab": "Al capturar llançament", "PackageVersion": "Versió del paquet", - "ProxyCheckFailedToTestMessage": "No s'ha pogut provar el servidor intermediari: {0}", - "ProxyCheckResolveIpMessage": "No s'ha pogut resoldre l'adreça IP de l'amfitrió intermediari configurat {0}", + "ProxyFailedToTestHealthCheckMessage": "No s'ha pogut provar el servidor intermediari: {url}", + "ProxyResolveIpHealthCheckMessage": "No s'ha pogut resoldre l'adreça IP de l'amfitrió intermediari configurat {proxyHostName}", "Queued": "En cua", "ReadTheWikiForMoreInformation": "Llegiu el Wiki per a més informació", "RestartRequiredHelpTextWarning": "Cal reiniciar perquè tingui efecte", "Restore": "Restaura", "RestoreBackup": "Restaura còpia de seguretat", - "RSS": "RSS", + "Rss": "RSS", "Save": "Desa", "SaveChanges": "Desa els canvis", "Security": "Seguretat", @@ -216,7 +216,7 @@ "SettingsShowRelativeDatesHelpText": "Mostra dates relatives (avui/ahir/etc) o absolutes", "ShowSearch": "Mostra la cerca", "ShowSearchHelpText": "Mostra el botó de cerca al passar el cursor", - "Shutdown": "Tanca", + "Shutdown": "Apaga", "Sort": "Ordena", "Source": "Font", "SSLCertPassword": "Contrasenya de certificat SSL", @@ -235,13 +235,13 @@ "Enable": "Activa", "IndexerFlags": "Indicadors de l'indexador", "UnableToLoadNotifications": "No es poden carregar les notificacions", - "IndexerLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores", - "IndexerLongTermStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {indexerNames}", "IndexerPriority": "Prioritat de l'indexador", "UnsavedChanges": "Canvis no desats", "UpdateAutomaticallyHelpText": "Baixeu i instal·leu les actualitzacions automàticament. Encara podreu instal·lar des de Sistema: Actualitzacions", - "UpdateCheckStartupTranslocationMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici \"{0}\" es troba en una carpeta de translocació d'aplicacions.", - "UpdateCheckUINotWritableMessage": "No es pot instal·lar l'actualització perquè l'usuari '{1}' no pot escriure la carpeta de la IU '{0}'.", + "UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.", + "UpdateUiNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no pot escriure la carpeta de la IU '{uiFolder}'.", "UpdateScriptPathHelpText": "Camí a un script personalitzat que pren un paquet d'actualització i gestiona la resta del procés d'actualització", "Uptime": "Temps de funcionament", "Info": "Informació", @@ -251,7 +251,7 @@ "Level": "Nivell", "LogFiles": "Fitxers de registre", "Logging": "Registre", - "MappedDrivesRunningAsService": "Les unitats de xarxa assignades no estan disponibles quan s'executen com a servei de Windows. Si us plau, consulteu les PMF per obtenir més informació", + "MappedDrivesRunningAsService": "Les unitats de xarxa assignades no estan disponibles quan s'executen com a servei de Windows. Si us plau, consulteu les PMF per a obtenir més informació", "Mechanism": "Mecanisme", "MIA": "MIA", "Wiki": "Wiki", @@ -260,21 +260,21 @@ "Branch": "Branca", "Connections": "Connexions", "ConnectSettings": "Configuració de connexió", - "DeleteBackupMessageText": "Esteu segur que voleu suprimir la còpia de seguretat '{0}'?", + "DeleteBackupMessageText": "Esteu segur que voleu suprimir la còpia de seguretat '{name}'?", "DeleteDownloadClient": "Suprimeix el client de descàrrega", - "DeleteDownloadClientMessageText": "Esteu segur que voleu suprimir el client de baixada '{0}'?", + "DeleteDownloadClientMessageText": "Esteu segur que voleu suprimir el client de baixada '{name}'?", "Discord": "Discord", "Docker": "Docker", "Donations": "Donacions", - "DownloadClientStatusCheckSingleClientMessage": "Baixa els clients no disponibles a causa d'errors: {0}", - "HealthNoIssues": "No hi ha cap problema amb la configuració", + "DownloadClientStatusSingleClientHealthCheckMessage": "Baixa els clients no disponibles a causa d'errors: {downloadClientNames}", + "NoIssuesWithYourConfiguration": "No hi ha cap problema amb la configuració", "HideAdvanced": "Amaga avançat", "History": "Història", "HomePage": "Pàgina d'inici", "Hostname": "Nom d'amfitrió", "IgnoredAddresses": "Adreces ignorades", "ProxyUsernameHelpText": "Només cal que introduïu un nom d'usuari i una contrasenya si cal. Deixeu-los en blanc en cas contrari.", - "RSSIsNotSupportedWithThisIndexer": "RSS no és compatible amb aquest indexador", + "RssIsNotSupportedWithThisIndexer": "RSS no és compatible amb aquest indexador", "SaveSettings": "Desa la configuració", "Seeders": "Llavors", "SelectAll": "Selecciona-ho tot", @@ -294,88 +294,451 @@ "UnableToAddANewNotificationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.", "UnableToLoadGeneralSettings": "No es pot carregar la configuració general", "UnableToLoadHistory": "No es pot carregar l'historial", - "UpdateCheckStartupNotWritableMessage": "No es pot instal·lar l'actualització perquè l'usuari \"{1}\" no pot escriure la carpeta d'inici \"{0}\".", + "UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.", "URLBase": "Base URL", "Usenet": "Usenet", "View": "Visualitza", "Yesterday": "Ahir", - "ApplicationStatusCheckSingleClientMessage": "Llistes no disponibles a causa d'errors: {0}", - "AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió del temps d'execució. Utilitzarem aquesta informació per prioritzar les funcions i les correccions d'errors.", + "ApplicationStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors: {0}", + "AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de {appName}. Això inclou informació sobre el vostre navegador, quines pàgines {appName} WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió de l'entorn d'execució. Utilitzarem aquesta informació per a prioritzar les funcions i les correccions d'errors.", "HistoryCleanupDaysHelpTextWarning": "Els fitxers de la paperera de reciclatge més antics que el nombre de dies seleccionat es netejaran automàticament", - "UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil de qualitat nou, torneu-ho a provar.", - "BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData del {appName}", - "AllIndexersHiddenDueToFilter": "Totes les pel·lícules estan ocultes a causa del filtre aplicat.", + "UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil d'aplicació nou, torneu-ho a provar.", + "BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData de {appName}", + "AllIndexersHiddenDueToFilter": "Tots els indexadors estan ocults a causa del filtre aplicat.", "EnableRss": "Activa RSS", "Grabs": "Captura", "EnableAutomaticSearchHelpText": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per {appName}", - "UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.", + "UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una aplicació nova, torneu-ho a provar.", "Application": "Aplicacions", "Applications": "Aplicacions", - "ApplicationStatusCheckAllClientMessage": "Totes les llistes no estan disponibles a causa d'errors", - "AuthenticationMethodHelpText": "Requereix nom d'usuari i contrasenya per accedir al radar", - "ApplicationLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores", - "ApplicationLongTermStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {0}", + "ApplicationStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors", + "AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}", + "ApplicationLongTermStatusCheckAllClientMessage": "Totes les aplicacions no estan disponibles a causa d'errors durant més de 6 hores", + "ApplicationLongTermStatusCheckSingleClientMessage": "Aplicacions no disponibles a causa d'errors durant més de 6 hores: {0}", "BindAddressHelpText": "Adreça IP vàlida, localhost o '*' per a totes les interfícies", - "BranchUpdate": "Branca que s'utilitza per actualitzar {appName}", + "BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}", "Connect": "Notificacions", - "DeleteApplicationMessageText": "Esteu segur que voleu suprimir la notificació '{0}'?", - "DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir la llista '{0}'?", + "DeleteApplicationMessageText": "Esteu segur que voleu suprimir l'aplicació '{name}'?", + "DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir el servidor intermediari de l'indexador '{name}'?", "Encoding": "Codificació", - "ForMoreInformationOnTheIndividualDownloadClients": "Per obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.", + "ForMoreInformationOnTheIndividualDownloadClients": "Per a obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.", "GeneralSettingsSummary": "Port, SSL, nom d'usuari/contrasenya, servidor intermediari, analítiques i actualitzacions", - "GrabReleases": "Captura novetat", + "GrabReleases": "Captura llançament(s)", "HistoryCleanupDaysHelpText": "Establiu a 0 per desactivar la neteja automàtica", "Notification": "Notificacions", "Notifications": "Notificacions", "ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de {appName} vàlida, no rebreu actualitzacions", - "TagsHelpText": "S'aplica a pel·lícules amb almenys una etiqueta coincident", - "Torrent": "Torrent", - "UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un indexador nou, torneu-ho a provar.", + "TagsHelpText": "S'aplica a indexadors amb almenys una etiqueta coincident", + "Torrent": "Torrents", + "UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un servidor intermediari d'indexador nou, torneu-ho a provar.", "UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de {appName} o un script", "UserAgentProvidedByTheAppThatCalledTheAPI": "Agent d'usuari proporcionat per l'aplicació per fer peticions a l'API", - "IndexerProxyStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors", - "IndexerProxyStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tots els indexadors no estan disponibles a causa d'errors", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Els indexadors no estan disponibles a causa d'errors: {indexerProxyNames}", "LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de {appName} a l'inici de l'aplicació.", - "Link": "Enllaços", + "Link": "Enllaç", "UILanguageHelpText": "Idioma que utilitzarà {appName} per a la interfície d'usuari", "Remove": "Elimina", "Replace": "Substitueix", - "TheLatestVersionIsAlreadyInstalled": "La darrera versió de {0} ja està instal·lada", - "ThemeHelpText": "Canvieu el tema de la interfície d'usuari de l'aplicació, el tema \"Automàtic\" utilitzarà el tema del vostre sistema operatiu per configurar el mode clar o fosc. Inspirat en Theme.Park", + "OnLatestVersion": "La darrera versió de {appName} ja està instal·lada", + "ThemeHelpText": "Canvieu el tema de la interfície d'usuari de l'aplicació, el tema \"Automàtic\" utilitzarà el tema del vostre sistema operatiu per configurar el mode clar o fosc. Inspirat en {inspiredBy}.", "ApplicationURL": "URL de l'aplicació", "Publisher": "Editor", - "ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base", - "ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent", - "ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les pel·lícules seleccionades", - "ApplyTagsHelpTextHowToApplyIndexers": "Com aplicar etiquetes a les pel·lícules seleccionades", - "ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes", - "DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?", + "ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base", + "ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent", + "ApplyTagsHelpTextHowToApplyApplications": "Com aplicar etiquetes a les aplicacions seleccionades", + "ApplyTagsHelpTextHowToApplyIndexers": "Com aplicar etiquetes als indexadors seleccionats", + "ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes", + "DeleteSelectedApplicationsMessageText": "Esteu segur que voleu suprimir {count} sol·licituds seleccionades?", "Label": "Etiqueta", - "ApplyTagsHelpTextReplace": "Substituïu: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per esborrar totes les etiquetes)", - "DeleteSelectedDownloadClients": "Suprimeix el client de descàrrega", + "ApplyTagsHelpTextReplace": "Substitució: substituïu les etiquetes per les etiquetes introduïdes (no introduïu cap etiqueta per a esborrar totes les etiquetes)", + "DeleteSelectedDownloadClients": "Suprimeix el(s) client(s) de baixada", "Genre": "Gèneres", - "DeleteSelectedDownloadClientsMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?", - "DeleteSelectedIndexersMessageText": "Esteu segur que voleu suprimir l'indexador '{0}'?", + "DeleteSelectedDownloadClientsMessageText": "Esteu segur que voleu suprimir {count} client(s) de baixada seleccionat(s)?", + "DeleteSelectedIndexersMessageText": "Esteu segur que voleu suprimir {count} indexador(s) seleccionat(s)?", "DownloadClientPriorityHelpText": "Prioritzeu diversos clients de baixada. S'utilitza round-robin per a clients amb la mateixa prioritat.", "More": "Més", - "Season": "temporada", + "Season": "Temporada", "Theme": "Tema", - "Track": "Traça", + "Track": "Pista", "Year": "Any", - "UpdateAvailable": "Nova actualització disponible", - "ConnectionLostReconnect": "Radarr intentarà connectar-se automàticament, o podeu fer clic a recarregar.", - "ConnectionLostToBackend": "Radarr ha perdut la connexió amb el backend i s'haurà de tornar a carregar per restaurar la funcionalitat.", + "UpdateAvailableHealthCheckMessage": "Nova actualització disponible", + "ConnectionLostReconnect": "{appName} intentarà connectar-se automàticament, o podeu fer clic a recarregar.", + "ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.", "RecentChanges": "Canvis recents", - "WhatsNew": "Que hi ha de nou?", - "minutes": "Minuts", - "DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de qualitat {0}", - "NotificationStatusSingleClientHealthCheckMessage": "Llistes no disponibles a causa d'errors: {0}", - "AddConnection": "Edita la col·lecció", - "NotificationStatusAllClientHealthCheckMessage": "Totes les llistes no estan disponibles a causa d'errors", - "AuthBasic": "Basic (finestra emergent del navegador)", + "WhatsNew": "Novetats", + "minutes": "minuts", + "DeleteAppProfileMessageText": "Esteu segur que voleu suprimir el perfil de l'aplicació '{name}'?", + "NotificationStatusSingleClientHealthCheckMessage": "Notificacions no disponibles a causa d'errors: {notificationNames}", + "AddConnection": "Afegeix una connexió", + "NotificationStatusAllClientHealthCheckMessage": "Totes les notificacions no estan disponibles a causa d'errors", + "AuthBasic": "Bàsic (finestra emergent del navegador)", "AuthForm": "Formularis (pàgina d'inici de sessió)", "DisabledForLocalAddresses": "Desactivat per a adreces locals", "None": "Cap", - "ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?", - "RestartProwlarr": "Reinicia {appName}" + "ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?", + "RestartProwlarr": "Reinicia {appName}", + "AuthenticationRequired": "Autenticació necessària", + "CountDownloadClientsSelected": "{count} client(s) de baixada seleccionat(s)", + "NoDownloadClientsFound": "No s'han trobat clients de baixada", + "AuthenticationRequiredWarning": "Per a evitar l'accés remot sense autenticació, ara {appName} requereix que l'autenticació estigui activada. Opcionalment, podeu desactivar l'autenticació des d'adreces locals.", + "AppUpdatedVersion": "{appName} s'ha actualitzat a la versió `{version}`, per tal d'obtenir els darrers canvis, haureu de tornar a carregar {appName}", + "AppUpdated": "{appName} Actualitzada", + "ApplyChanges": "Aplica els canvis", + "Implementation": "Implementació", + "OnHealthRestored": "Al resoldre les incidències", + "ManageDownloadClients": "Gestiona els clients de descàrrega", + "AuthenticationRequiredHelpText": "Canvia per a quines sol·licituds cal autenticar. No canvieu tret que entengueu els riscos.", + "CountIndexersSelected": "S'han seleccionat {count} indexador(s)", + "EditDownloadClientImplementation": "Edita el client de baixada - {implementationName}", + "EditIndexerImplementation": "Edita l'indexador - {implementationName}", + "EditSelectedDownloadClients": "Editeu els clients de descàrrega seleccionats", + "EditSelectedIndexers": "Edita els indexadors seleccionats", + "IndexerDownloadClientHealthCheckMessage": "Indexadors amb clients de baixada no vàlids: {indexerNames}.", + "AddCustomFilter": "Afegeix un filtre personalitzat", + "AddDownloadClientImplementation": "Afegeix un client de descàrrega - {implementationName}", + "AddIndexerImplementation": "Afegeix un indexador - {implementationName}", + "AddConnectionImplementation": "Afegeix una connexió - {implementationName}", + "InvalidUILanguage": "La vostra IU està configurada en un idioma no vàlid, corregiu-lo i deseu la configuració", + "NoHistoryFound": "No s'ha trobat cap historial", + "NoIndexersFound": "No s'han trobat indexadors", + "OnHealthRestoredHelpText": "Al resoldre les incidències", + "AuthenticationMethod": "Mètode d'autenticació", + "AuthenticationMethodHelpTextWarning": "Seleccioneu un mètode d'autenticació vàlid", + "AuthenticationRequiredPasswordHelpTextWarning": "Introduïu una contrasenya nova", + "DefaultNameCopiedProfile": "{name} - Còpia", + "DownloadClientQbittorrentSettingsContentLayout": "Disseny de contingut", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si s'utilitza el disseny de contingut de qBittorrent s'utilitza el disseny original del torrent o es crea una subcarpeta (qBittorrent 4.3.2+)", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmeu la nova contrasenya", + "AuthenticationRequiredUsernameHelpTextWarning": "Introduïu un nom d'usuari nou", + "Categories": "Categories", + "ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració", + "Episode": "Episodi", + "EditApplicationImplementation": "Edita l'aplicació - {implementationName}", + "EditConnectionImplementation": "Afegeix una connexió - {implementationName}", + "EditIndexerProxyImplementation": "Edita el servidor intermediari de l'indexador - {implementationName}", + "days": "dies", + "Album": "Àlbum", + "Artist": "Artista", + "AddApplicationImplementation": "Afegeix una aplicació - {implementationName}", + "AddIndexerProxyImplementation": "Afegeix un indexador - {implementationName}", + "Category": "Categoria", + "Clone": "Clona", + "Yes": "Si", + "No": "No", + "StopSelecting": "Deixa de seleccionar", + "External": "Extern", + "Id": "ID", + "Author": "Autor", + "ManageClients": "Gestiona els clients", + "CountApplicationsSelected": "{count} aplicacions seleccionades", + "DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2", + "Donate": "Dona", + "BlackholeFolderHelpText": "Carpeta on {appName} emmagatzemarà els fitxers {extension}", + "Destination": "Destinació", + "Directory": "Directori", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}", + "CustomFilter": "Filtres personalitzats", + "IndexerHDBitsSettingsCodecs": "Còdecs", + "DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {clientName}, com ara {url}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de rTorrent", + "IndexerHDBitsSettingsMediums": "Mitjans", + "Query": "Consulta", + "AddApplication": "Afegeix una aplicació", + "AddRemoveOnly": "Només afegeix i elimina", + "AddSyncProfile": "Afegeix perfil de sincronització", + "Auth": "Autenticació", + "ActiveIndexers": "Indexadors actius", + "Description": "Descripció", + "DeleteSelectedIndexers": "Suprimeix els indexadors seleccionats", + "IndexerSettingsBaseUrlHelpText": "Seleccioneu quina URL base utilitzarà {appName} per a les sol·licituds al lloc", + "ActiveApps": "Aplicacions actives", + "AddCategory": "Afegeix una categoria", + "AddDownloadClientToProwlarr": "Afegir un client de baixada permet que {appName} enviï publicacions directament des de la interfície d'usuari mentre fa una cerca manual.", + "AddIndexerProxy": "Afegeix un servidor intermediari per a l'indexador", + "AddNewIndexer": "Afegeix un nou indexador", + "AddToDownloadClient": "Afegeix llançament al client de baixades", + "AddedToDownloadClient": "Baixada afegida al client", + "DevelopmentSettings": "Configuració de desenvolupament", + "Mixed": "Combinat", + "Apps": "Aplicacions", + "Privacy": "Privacitat", + "Redirect": "Redirecció", + "Stats": "Estadístiques", + "Private": "Privat", + "Proxies": "Servidors intermediaris", + "Public": "Públic", + "DeleteSelectedIndexer": "Suprimeix els indexadors seleccionats", + "EditSyncProfile": "Afegeix perfil de sincronització", + "Menu": "Menú", + "OnGrabHelpText": "Al capturar llançament", + "ProxyValidationBadRequest": "No s'ha pogut provar el servidor intermediari. Codi d'estat: {statusCode}", + "Default": "Per defecte", + "GrabRelease": "Captura novetat", + "ManualGrab": "Captura manual", + "PrioritySettings": "Prioritat: {priority}", + "Any": "Qualsevol", + "BuiltIn": "Integrat", + "Script": "Script", + "InfoUrl": "URL d'informació", + "PublishedDate": "Data de publicació", + "Redirected": "Redirecció", + "AllSearchResultsHiddenByFilter": "Tots els resultats estan ocults pel filtre aplicat.", + "HealthMessagesInfoBox": "Podeu trobar més informació sobre la causa d'aquests missatges de comprovació de salut fent clic a l'enllaç wiki (icona del llibre) al final de la fila o consultant els vostres [registres]({link}). Si teniu problemes per a interpretar aquests missatges, podeu posar-vos en contacte amb el nostre suport als enllaços següents.", + "AptUpdater": "Utilitzeu apt per a instal·lar l'actualització", + "DockerUpdater": "actualitzeu el contenidor Docker per a rebre l'actualització", + "Download": "Baixa", + "ErrorRestoringBackup": "S'ha produït un error en restaurar la còpia de seguretat", + "ExternalUpdater": "{appName} està configurat per a utilitzar un mecanisme d'actualització extern", + "FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions", + "LogFilesLocation": "Els fitxers de registre es troben a: {location}", + "Logout": "Tanca la sessió", + "NoEventsFound": "No s'han trobat esdeveniments", + "RestartReloadNote": "Nota: {appName} es reiniciarà i tornarà a carregar automàticament la interfície d'usuari durant el procés de restauració.", + "TheLogLevelDefault": "El nivell de registre per defecte és \"Info\" i es pot canviar a [Configuració general](/configuració/general)", + "UpdateAppDirectlyLoadError": "No es pot actualitzar {appName} directament,", + "WouldYouLikeToRestoreBackup": "Voleu restaurar la còpia de seguretat '{name}'?", + "InstallLatest": "Instal·la l'últim", + "CurrentlyInstalled": "Instal·lat actualment", + "DownloadClientSettingsAddPaused": "Afegeix pausats", + "Install": "Instal·la", + "DownloadClientFloodSettingsAdditionalTags": "Etiquetes addicionals", + "DownloadClientFreeboxSettingsApiUrl": "URL de l'API", + "DownloadClientFreeboxSettingsAppId": "Identificador de l'aplicació", + "PreviouslyInstalled": "Instal·lat anteriorment", + "PasswordConfirmation": "Confirmeu la contrasenya", + "IndexerHDBitsSettingsOriginsHelpText": "Si no s'especifica, s'utilitzen totes les opcions.", + "MinimumSeeders": "Seeders mínims", + "SeedRatio": "Ràtio de la llavor", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Si un torrent està bloquejat per un hash, pot ser que no es rebutgi correctament durant el RSS/Search per a alguns indexadors, habilitant això permetrà que es rebutgi després que s'agafi el torrent, però abans que s'enviï al client.", + "SeedTime": "Temps de la llavor", + "DefaultCategory": "Categoria predeterminada", + "IndexerVipExpiredHealthCheckMessage": "Els beneficis VIP de l'indexador han caducat: {indexerNames}", + "MassEditor": "Editor de masses", + "NoSearchResultsFound": "No s'han trobat resultats de cerca, proveu de realitzar una nova cerca a continuació.", + "NotSupported": "No suportat", + "OverrideAndAddToDownloadClient": "Sobreescriu i afegeix al client de baixada", + "PackSeedTime": "Temps de la llavor del paquet", + "SearchAllIndexers": "Cerca tots els indexadors", + "SearchIndexers": "Cerca indexadors", + "SeedTimeHelpText": "L'hora en què un torrent s'ha de sembrar abans d'aturar-se, buit és el predeterminat de l'aplicació", + "SettingsIndexerLogging": "Registre de l'indexador millorat", + "TotalIndexerSuccessfulGrabs": "Indexador total correcte", + "BookSearch": "Cerca de llibres", + "ClearHistoryMessageText": "Esteu segur que voleu netejar tot l'historial de {appName}?", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "ClearHistory": "Neteja l'historial", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Clau de l'API del lloc", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Clau API del lloc (trobada a Configuració => Accés)", + "IndexerBeyondHDSettingsRssKeyHelpText": "Clau RSS del lloc (trobada a Seguretat => Clau RSS)", + "ApplicationsLoadError": "No s'ha pogut carregar la llista d'aplicacions", + "BookSearchTypes": "Tipus de cerca de llibres", + "DownloadClientCategory": "Baixa la categoria del client", + "IndexerAuth": "Autor de l'indexador", + "IndexerBeyondHDSettingsLimitedOnly": "Només limitat", + "IndexerBeyondHDSettingsRefundOnly": "Només el reemborsament", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Cerca només el reemborsament", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Cerca només el rebobinat", + "IndexerBeyondHDSettingsSearchTypes": "Tipus de cerca", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Seleccioneu els tipus de llançaments que us interessin. Si no hi ha cap seleccionat, s'utilitzen totes les opcions.", + "IndexerDownloadClientHelpText": "Especifiqueu quin client de baixada s'utilitza per a les captures fetes a {appName} des d'aquest indexador", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Ha de tenir permisos d'usuari i torrents", + "IndexerGazelleGamesSettingsSearchGroupNames": "Cerca noms de grup", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostra només els llançaments de freeleech", + "IndexerHDBitsSettingsUseFilenames": "Utilitza els noms de fitxer", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Marqueu aquesta opció si voleu utilitzar noms de fitxer torrent com a títols de llançament", + "IndexerHDBitsSettingsUsernameHelpText": "Nom d'usuari del lloc", + "IndexerHealthCheckNoIndexers": "No hi ha indexadors activats, {appName} no retornarà els resultats de la cerca", + "IndexerHistoryLoadError": "Error en carregar l'historial de l'indexador", + "IndexerIPTorrentsSettingsCookieUserAgent": "Agent d'usuari de la galeta", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "IndexerInfo": "Informació de l'indexador", + "IndexerNebulanceSettingsApiKeyHelpText": "Clau API de la Configuració de l'usuari . Claus Api. La clau ha de tenir permisos de llista i baixada", + "IndexerNewznabSettingsVipExpirationHelpText": "Data d'entrada (yyyy-mm-dd) per a la caducitat VIP o en blanc, {appName} notificarà 1 setmana des de la caducitat de VIP", + "IndexerNoDefinitionCheckHealthCheckMessage": "Els indexadors no tenen definició i no funcionaran: {indexerNames}. Suprimiu i (o torneu a afegir) a {appName}.", + "IndexerObsoleteCheckMessage": "Els indexadors estan obsolets o s'han actualitzat: {0}. Suprimiu i (o torneu a afegir) a {appName}", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Clau de l'API del lloc", + "IndexerRss": "Indexador RSS", + "IndexerSettingsGrabLimitHelpText": "El nombre màxim de captures especificat per la unitat respectiva que {appName} permetrà al lloc", + "IndexerSettingsPackSeedTimeIndexerHelpText": "L'hora en què un paquet (temporada o discografia) s'ha de sembrar el torrent abans d'aturar-se, buit és el valor predeterminat de l'aplicació", + "IndexerSettingsPreferMagnetUrl": "Prefereix l'URL de l'imant", + "IndexerSettingsPreferMagnetUrlHelpText": "Quan està activat, aquest indexador preferirà l'ús d'URLs magnet per a les captures amb enllaços de reserva a torrents", + "IndexerSettingsQueryLimitHelpText": "El nombre màxim de consultes especificat per la unitat respectiva que {appName} permetrà al lloc", + "InitialFailure": "Fallada inicial", + "NoIndexerCategories": "No s'ha trobat cap categoria per a aquest indexador", + "RepeatSearch": "Cerca repetida", + "SettingsLogRotateHelpText": "Nombre màxim de fitxers de registre a mantenir desats a la carpeta de registres", + "SyncProfile": "Perfil de sincronització", + "TotalUserAgentGrabs": "Total d'agents d'usuari", + "UnableToLoadAppProfiles": "No s'han pogut carregar els perfils de l'aplicació", + "VipExpiration": "Caducitat VIP", + "IndexerOrpheusSettingsApiKeyHelpText": "Clau API del lloc (trobada a Configuració => Accés)", + "IndexerRedactedSettingsApiKeyHelpText": "Clau API del lloc (trobada a Configuració => Accés)", + "ProwlarrSupportsAnyIndexer": "{appName} admet molts indexadors, a més de qualsevol indexador que utilitzi l'estàndard Newznab/Torznab utilitzant 'Generic Newznab' (per usenet) o 'Generic Torznab' (per torrents). Cerca i selecciona el teu indexador des de sota.", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Categoria alternativa predeterminada si no hi ha cap categoria assignada per a un llançament. Afegir una categoria específica a {appName} evita conflictes amb baixades no relacionades amb {appName}. L'ús d'una categoria és opcional, però molt recomanable.", + "AppProfileSelectHelpText": "Els perfils d'aplicació s'utilitzen per controlar RSS, la cerca automàtica i la configuració de cerca interactiva en sincronitzar aplicacions", + "AudioSearch": "Cerca d'àudio", + "IndexerSettingsAppsMinimumSeeders": "Aplicacions de cercadors mínims", + "DeleteApplication": "Suprimeix l'aplicació", + "DownloadClientSettingsPriorityItemHelpText": "Prioritat a usar en capturar elements", + "DownloadClientSettingsDefaultCategoryHelpText": "Categoria alternativa predeterminada si no hi ha cap categoria assignada per a un llançament. Afegir una categoria específica a {appName} evita conflictes amb baixades no relacionades amb {appName}. L'ús d'una categoria és opcional, però molt recomanable.", + "EditCategory": "Edita la categoria", + "IndexerQuery": "Consulta de l'indexador", + "IndexerSettingsBaseUrl": "Url base", + "IndexerSettingsCookieHelpText": "Cookie del lloc", + "IndexerSettingsLimitsUnitHelpText": "La unitat de temps per comptar els límits per indexador", + "IndexerSettingsPackSeedTime": "Temps de la llavor del paquet", + "IndexerSite": "Lloc indexador", + "IndexerTagsHelpTextWarning": "Les etiquetes s'han d'utilitzar amb precaució, poden tenir efectes no desitjats. Un indexador amb una etiqueta només sincronitzarà amb aplicacions amb la mateixa etiqueta.", + "IndexerSettingsSummary": "Configura diversos paràmetres globals de l'indexador.", + "ManageApplications": "Gestiona les aplicacions", + "PackSeedTimeHelpText": "L'hora en què un paquet (temporada o discografia) s'ha de sembrar el torrent abans d'aturar-se, buit és el valor predeterminat de l'aplicació", + "PreferMagnetUrl": "Prefereix l'URL de l'imant", + "PreferMagnetUrlHelpText": "Quan està activat, aquest indexador preferirà l'ús d'URLs magnet per a les captures amb enllaços de reserva a torrents", + "ProwlarrDownloadClientsInAppOnlyAlert": "Els clients de baixada només són per a les cerques a l'aplicació {appName} i no sincronitzen amb les aplicacions. No hi ha plans per afegir aquesta funcionalitat.", + "IndexerBeyondHDSettingsRewindOnly": "Només rebobina", + "ProxyValidationUnableToConnect": "No s'ha pogut connectar al servidor intermediari: {exceptionMessage}. Comprova els detalls del registre que envolta aquest error", + "QueryOptions": "Opcions de la consulta", + "TestAllApps": "Prova totes les aplicacions", + "TotalHostQueries": "Total de consultes de l'amfitrió", + "AverageGrabs": "Mitjana d'herba", + "AverageQueries": "Mitjana de consultes", + "FilterPlaceHolder": "Cerca indexadors", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Cerca només freeleech (Limited UL)", + "IndexerBeyondHDSettingsApiKeyHelpText": "Clau API del lloc (trobada a Seguretat => Clau API)", + "IndexerSettingsVipExpiration": "Caducitat VIP", + "IndexerVipExpiringHealthCheckMessage": "Els beneficis VIP de l'indexador expiraran aviat: {indexerNames}", + "LastFailure": "Darrera fallada", + "MovieSearch": "Cerca de pel·lícules", + "MovieSearchTypes": "Tipus de cerca de pel·lícules", + "MusicSearchTypes": "Tipus de cerca de música", + "QueryType": "Tipus de consulta", + "RssQueries": "Consultes RSS", + "SyncLevelFull": "Sincronització completa: mantindrà els indexadors d'aquesta aplicació completament sincronitzats. Els canvis fets als indexadors a {appName} se sincronitzen amb aquesta aplicació. Qualsevol canvi fet a indexadors remotament dins d'aquesta aplicació serà anul·lat per {appName} en la següent sincronització.", + "TotalHostGrabs": "Total d'amfitrions", + "TotalQueries": "Total de consultes", + "NoApplicationsFound": "No s'ha trobat cap aplicació", + "SyncProfiles": "Sincronitza els perfils", + "TorznabUrl": "Url Torznab", + "TvSearch": "Cerca de TV", + "DeleteIndexerProxy": "Suprimeix el servidor intermediari de l'indexador", + "DisabledUntil": "Desactivat fins", + "GrabTitle": "Captura el títol", + "SettingsIndexerLoggingHelpText": "Registra dades addicionals de l'indexador", + "SyncLevel": "Nivell de sincronització", + "AdvancedSettingsHiddenClickToShow": "Configuració avançada oculta, feu clic per mostrar", + "AdvancedSettingsShownClickToHide": "Configuració avançada mostrada, feu clic per amagar", + "AppsMinimumSeeders": "Aplicacions de cercadors mínims", + "AppsMinimumSeedersHelpText": "«Mínims filtradors requerits per les Aplicacions perquè l'indexador s'agafi", + "CountIndexersAvailable": "{count} indexador(s) disponible", + "HistoryCleanup": "Neteja de l'historial", + "HistoryDetails": "Detalls de l'historial", + "SettingsFilterSentryEventsHelpText": "Filtra els esdeveniments d'error d'usuari coneguts perquè s'enviïn com a Analytics", + "MappedCategories": "Categories assignades", + "AppSettingsSummary": "Aplicacions i paràmetres per configurar com {appName} interactua amb els vostres programes PVR", + "ConnectSettingsSummary": "Notificacions i scripts personalitzats", + "DeleteClientCategory": "Suprimeix la categoria del client de baixada", + "FullSync": "Sincronització completa", + "IndexerAlreadySetup": "Almenys una instància de l'indexador ja està configurada", + "RawSearchSupported": "S'admet la cerca RAW", + "RssFeed": "Canal RSS", + "SearchTypes": "Tipus de cerca", + "SeedRatioHelpText": "La relació a la qual ha d'arribar un torrent abans d'aturar-se, buida és la predeterminada de l'aplicació", + "SemiPrivate": "Semi-Privada", + "TotalIndexerQueries": "Total de consultes de l'indexador", + "TotalUserAgentQueries": "Total de consultes d'agents d'usuari", + "GoToApplication": "Ves a l'aplicació", + "Url": "Url", + "AreYouSureYouWantToDeleteIndexer": "Esteu segur que voleu suprimir '{name}' de {appName}?", + "AverageResponseTimesMs": "Temps mitjà de resposta de l'indexador (ms)", + "FoundCountReleases": "S'han trobat {itemCount} versions", + "AuthQueries": "Consultes d'Autorització", + "BasicSearch": "Cerca bàsica", + "IndexerName": "Nom de l'indexador", + "IndexerStatus": "Estat de l'indexador", + "IndexerTagsHelpText": "Utilitzeu etiquetes per especificar els intermediaris de l'indexador o a quines aplicacions se sincronitza l'indexador.", + "NewznabUrl": "Url Newznab", + "SearchCountIndexers": "Cerca {count} indexador", + "UnableToLoadDevelopmentSettings": "No s'han pogut carregar els paràmetres de desenvolupament", + "SettingsFilterSentryEvents": "Filtra els esdeveniments d'anàlisi", + "ApplicationTagsHelpText": "Sincronitza els indexadors d'aquesta aplicació que tenen una o més etiquetes coincidents. Si no es llisten etiquetes aquí, llavors no s'impedirà la sincronització d'indexadors a causa de les seves etiquetes.", + "ApplicationTagsHelpTextWarning": "Les etiquetes s'han d'utilitzar amb precaució, poden tenir efectes no desitjats. Una aplicació amb una etiqueta només sincronitzarà amb els indexadors que tinguin la mateixa etiqueta.", + "DeleteSelectedApplications": "Suprimeix les aplicacions seleccionades", + "DownloadClientsSettingsSummary": "Baixa la configuració dels clients per a la integració a la cerca de la interfície d'usuari {appName}", + "ElapsedTime": "Temps transcorregut", + "EnableIndexer": "Habilita l'indexador", + "EnableRssHelpText": "Habilita el canal RSS per a l'indexador", + "IncludeManualGrabsHelpText": "Inclou les notes manuals fetes a {appName}", + "IndexerFailureRate": "Taxa de fallada de l'indexador", + "ProwlarrSupportsAnyDownloadClient": "{appName} admet qualsevol dels clients de baixada que es llisten a continuació.", + "TotalGrabs": "Grabs totals", + "IndexerDetails": "Detalls de l'indexador", + "IndexerPriorityHelpText": "Prioritat de l'indexador des de l'1 (el més alt) fins al 50 (el més oest). Per defecte: 25.", + "IndexerProxy": "Servidor intermediari de l'indexador", + "UISettingsSummary": "Opcions de data, idioma i color defectuoses", + "IndexerCategories": "Categories de l'indexador", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "SearchCapabilities": "Capacitats de cerca", + "SearchType": "Tipus de cerca", + "SettingsLogSql": "Registre Sql", + "SyncLevelAddRemove": "Afegeix i elimina només: quan s'afegeixen o s'eliminen els indexadors de {appName}, s'actualitzarà aquesta aplicació remota.", + "IndexerProxies": "Propis de l'indexador", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sincronitza les fulles del torrent de la llista de blocs en bloc mentre s'agafa", + "CertificateValidationHelpText": "Canvia l'estricta validació de la certificació HTTPS", + "IndexerDisabled": "Indexador desactivat", + "IndexerFileListSettingsPasskeyHelpText": "Contrasenya del lloc (Aquesta és la cadena alfanumèrica a l'URL del seguidor que es mostra al client de baixada)", + "IndexerSettingsQueryLimit": "Límit de consulta", + "IndexerHDBitsSettingsPasskeyHelpText": "Contrasenya dels detalls de l'usuari", + "IndexerSettingsPasskey": "Clau de pas", + "ClickToChangeQueryOptions": "Feu clic per a canviar les opcions de consulta", + "EnabledRedirected": "Activat, redirigit", + "Parameters": "Paràmetres", + "QueryResults": "Resultats de la consulta", + "RedirectHelpText": "Redirigeix la sol·licitud de baixada entrant per a l'indexador i passa la captura directament en lloc de intermediaris a través de {appName}", + "UnableToLoadIndexerProxies": "No s'han pogut carregar els intermediaris de l'indexador", + "IndexerId": "ID de l'indexador", + "IndexerAlphaRatioSettingsExcludeScene": "Exclou l'escena", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Exclou els llançaments d'escenes dels resultats", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "IndexerFileListSettingsUsernameHelpText": "Nom d'usuari del lloc", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Cerca publicacions per noms de grup", + "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", + "IndexerSettingsAppsMinimumSeedersHelpText": "«Mínims filtradors requerits per les Aplicacions perquè l'indexador s'agafi", + "IndexerSettingsFreeleechOnly": "Només Freeleech", + "IndexerSettingsGrabLimit": "Límit de captura", + "IndexerSettingsLimitsUnit": "Unitats de límits", + "IndexerSettingsRssKey": "Clau RSS", + "SelectIndexers": "Selecciona els indexadors", + "SettingsSqlLoggingHelpText": "Registra totes les consultes SQL de {appName}", + "SyncAppIndexers": "Sincronitza els indexadors d'aplicacions", + "AppProfileInUse": "Perfil d'aplicació en ús", + "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)", + "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", + "SettingsConsoleLogLevel": "Nivell de registre de la consola", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Només blat de moro daurat", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Cerca només els llançaments Golden Popcorn", + "Open": "Obre", + "ProwlarrDownloadClientsAlert": "Si voleu fer cerques directament dins de {appName}, heu d'afegir Clients de Baixades. En cas contrari, no cal afegir-les aquí. Per a les cerques des de les teves Apps, els clients de descàrrega configurats s'utilitzen en el seu lloc.", + "Website": "Lloc web", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Utilitza una connexió segura. Vegeu Opcions -> Interfície web -> 'Utilitza HTTPS en comptes d'HTTP' a qBittorrent.", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Cerca només els llançaments de freeleech", + "IndexerAvistazSettingsPasswordHelpText": "Contrasenya del lloc", + "IndexerAvistazSettingsPidHelpText": "PID de la pàgina del meu compte o del meu perfil", + "IndexerAvistazSettingsUsernameHelpText": "Nom d'usuari del lloc", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Només el rang de membre i superior pot utilitzar l'API en aquest indexador.", + "SelectedCountOfCountReleases": "S'han seleccionat {selectedCount} de les versions {itemCount}", + "SettingsLogRotate": "Rotació del registre", + "AreYouSureYouWantToDeleteCategory": "Esteu segur que voleu suprimir la categoria assignada?", + "Book": "Llibre" } diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index df2e95ac0..21975a02e 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -1,7 +1,7 @@ { "Add": "Přidat", - "CertificateValidation": "Ověření certifikátu", - "DeleteBackupMessageText": "Opravdu chcete smazat zálohu „{0}“?", + "CertificateValidation": "Ověřování certifikátu", + "DeleteBackupMessageText": "Opravdu chcete odstranit zálohu ‚{name}‘?", "YesCancel": "Ano, zrušit", "About": "O aplikaci", "Component": "Komponenta", @@ -18,12 +18,12 @@ "Usenet": "Usenet", "AddDownloadClient": "Přidat klienta pro stahování", "Backups": "Zálohy", - "CancelPendingTask": "Opravdu chcete zrušit tento nevyřízený úkol?", + "CancelPendingTask": "Opravdu chcete zrušit tento úkol čekající na vyřízení?", "MovieIndexScrollBottom": "Rejstřík filmů: Posun dolů", "ProxyType": "Typ serveru proxy", "Reddit": "Reddit", "ErrorLoadingContents": "Chyba při načítání obsahu", - "IndexerLongTermStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání po dobu delší než 6 hodin", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Všechny indexery jsou nedostupné z důvodu selhání déle než 6 hodin", "RemovedFromTaskQueue": "Odebráno z fronty úkolů", "ResetAPIKey": "Resetovat klíč API", "SSLCertPassword": "Heslo SSL Cert", @@ -35,27 +35,27 @@ "Warn": "Varovat", "Wiki": "Wiki", "Connections": "Připojení", - "DeleteDownloadClientMessageText": "Opravdu chcete odstranit klienta pro stahování „{0}“?", - "Details": "Detaily", + "DeleteDownloadClientMessageText": "Opravdu chcete odstranit klienta pro stahování ‚{name}‘?", + "Details": "Podrobnosti", "Disabled": "Zakázáno", - "Docker": "Přístavní dělník", + "Docker": "Docker", "Donations": "Dary", - "DownloadClientSettings": "Stáhněte si nastavení klienta", - "DownloadClientStatusCheckAllClientMessage": "Všichni klienti pro stahování nejsou kvůli chybám k dispozici", - "DownloadClientStatusCheckSingleClientMessage": "Stahování klientů není k dispozici z důvodu selhání: {0}", + "DownloadClientSettings": "Nastavení klienta pro stahování", + "DownloadClientStatusAllClientHealthCheckMessage": "Všichni klienti pro stahování jsou nedostupní z důvodu selhání", + "DownloadClientStatusSingleClientHealthCheckMessage": "Klienti pro stahování jsou nedostupní z důvodu selhání: {downloadClientNames}", "Folder": "Složka", - "Grabs": "Urvat", - "HealthNoIssues": "Žádné problémy s vaší konfigurací", + "Grabs": "Získané", + "NoIssuesWithYourConfiguration": "Žádné problémy s vaší konfigurací", "HideAdvanced": "Skrýt pokročilé", "Host": "Hostitel", "Hostname": "Název hostitele", - "IncludeHealthWarningsHelpText": "Zahrnout zdravotní varování", + "IncludeHealthWarningsHelpText": "Včetně varování ohledně zdraví", "Indexer": "Indexer", "IndexerFlags": "Příznaky indexeru", - "IndexerPriority": "Priorita indexování", - "IndexerPriorityHelpText": "Priorita indexování od 1 (nejvyšší) do 50 (nejnižší). Výchozí: 25.", + "IndexerPriority": "Priorita indexeru", + "IndexerPriorityHelpText": "Priorita indexeru od 1 (Nejvyšší) do 50 (Nejnižší). Výchozí: 25.", "Indexers": "Indexery", - "IndexerStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání", + "IndexerStatusAllUnavailableHealthCheckMessage": "Všechny indexery jsou nedostupné z důvodu selhání", "LastWriteTime": "Čas posledního zápisu", "Level": "Úroveň", "LogLevel": "Úroveň protokolu", @@ -67,25 +67,25 @@ "Ok": "OK", "SendAnonymousUsageData": "Odesílejte anonymní údaje o používání", "UnselectAll": "Odznačit vše", - "UpdateCheckStartupNotWritableMessage": "Aktualizaci nelze nainstalovat, protože spouštěcí složku „{0}“ nelze zapisovat uživatelem „{1}“.", + "UpdateStartupNotWritableHealthCheckMessage": "Aktualizaci nelze nainstalovat, protože spouštěcí složku „{startupFolder}“ nelze zapisovat uživatelem „{userName}“.", "Version": "Verze", - "AnalyticsEnabledHelpText": "Odesílejte anonymní informace o použití a chybách na servery {appName}u. To zahrnuje informace o vašem prohlížeči, které stránky {appName} WebUI používáte, hlášení chyb a také verzi operačního systému a běhového prostředí. Tyto informace použijeme k upřednostnění funkcí a oprav chyb.", + "AnalyticsEnabledHelpText": "Odesílejte anonymní informace o použití a chybách na servery {appName}u. To zahrnuje informace o vašem prohlížeči, které stránky webového rozhraní {appName}u používáte, hlášení chyb a také verzi operačního systému a běhového prostředí. Tyto informace použijeme k určení priorit funkcí a oprav chyb.", "ApiKey": "Klíč API", "AppDataDirectory": "Adresář AppData", "AppDataLocationHealthCheckMessage": "Aktualizace nebude možná, aby se zabránilo odstranění AppData při aktualizaci", - "ApplicationStatusCheckAllClientMessage": "Všechny seznamy nejsou k dispozici z důvodu selhání", - "ApplicationStatusCheckSingleClientMessage": "Seznamy nejsou k dispozici z důvodu selhání: {0}", + "ApplicationStatusCheckAllClientMessage": "Všechny aplikace jsou nedostupné z důvodu selhání", + "ApplicationStatusCheckSingleClientMessage": "Aplikace nedostupné z důvodu selhání: {0}", "Apply": "Použít", "Branch": "Větev", - "BranchUpdate": "Pobočka, která se má použít k aktualizaci {appName}", - "EditIndexer": "Upravit indexátor", + "BranchUpdate": "Větev použitá k aktualizaci {appName}u", + "EditIndexer": "Upravit indexer", "ForMoreInformationOnTheIndividualDownloadClients": "Další informace o jednotlivých klientech pro stahování získáte kliknutím na informační tlačítka.", - "General": "Všeobecné", - "CloseCurrentModal": "Zavřít aktuální modální", + "General": "Obecné", + "CloseCurrentModal": "Zavřít aktuální modální okno", "Columns": "Sloupce", - "ConnectionLost": "Spojení ztraceno", + "ConnectionLost": "Ztráta spojení", "ConnectSettings": "Nastavení připojení", - "Custom": "Zvyk", + "Custom": "Vlastní", "Error": "Chyba", "Failed": "Selhalo", "FeatureRequests": "Žádosti o funkce", @@ -93,9 +93,9 @@ "Files": "Soubory", "Filter": "Filtr", "Fixed": "Pevný", - "FocusSearchBox": "Zaostřovací vyhledávací pole", + "FocusSearchBox": "Zaměřit vyhledávací pole", "GeneralSettingsSummary": "Port, SSL, uživatelské jméno / heslo, proxy, analytika a aktualizace", - "History": "Dějiny", + "History": "Historie", "HomePage": "Domovská stránka", "SettingsEnableColorImpairedModeHelpText": "Upravený styl umožňující uživatelům s barevným postižením lépe rozlišovat barevně kódované informace", "SettingsLongDateFormat": "Long Date Format", @@ -105,8 +105,8 @@ "Tasks": "Úkoly", "Test": "Test", "UnableToLoadTags": "Značky nelze načíst", - "IndexerProxyStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání", - "ApplyTags": "Použít značky", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Všechny proxy indexerů jsou nedostupné z důvodu selhání", + "ApplyTags": "Použít štítky", "MoreInfo": "Více informací", "System": "Systém", "Enabled": "Povoleno", @@ -114,19 +114,19 @@ "AcceptConfirmationModal": "Přijměte potvrzovací modální okno", "Actions": "Akce", "Added": "Přidáno", - "AddIndexer": "Přidat indexátor", + "AddIndexer": "Přidat indexer", "LaunchBrowserHelpText": " Otevřete webový prohlížeč a při spuštění aplikace přejděte na domovskou stránku {appName}.", "Logging": "Protokolování", "Mechanism": "Mechanismus", "NoLinks": "Žádné odkazy", "Presets": "Předvolby", "Priority": "Přednost", - "Grabbed": "Popadl", + "Grabbed": "Získáno", "Health": "Zdraví", "LogLevelTraceHelpTextWarning": "Trasování protokolování by mělo být povoleno pouze dočasně", - "ProxyCheckBadRequestMessage": "Nepodařilo se otestovat proxy. StatusCode: {0}", - "ProxyCheckFailedToTestMessage": "Nepodařilo se otestovat proxy: {0}", - "ProxyCheckResolveIpMessage": "Nepodařilo se vyřešit adresu IP konfigurovaného hostitele proxy {0}", + "ProxyBadRequestHealthCheckMessage": "Nepodařilo se otestovat proxy. StatusCode: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Nepodařilo se otestovat proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Nepodařilo se vyřešit adresu IP konfigurovaného hostitele proxy {proxyHostName}", "ProxyPasswordHelpText": "Musíte pouze zadat uživatelské jméno a heslo, pokud je požadováno. Jinak je nechte prázdné.", "ProxyUsernameHelpText": "Musíte pouze zadat uživatelské jméno a heslo, pokud je požadováno. Jinak je nechte prázdné.", "Queue": "Fronta", @@ -161,8 +161,8 @@ "UnableToAddANewIndexerPleaseTryAgain": "Nelze přidat nový indexer, zkuste to znovu.", "UnableToAddANewIndexerProxyPleaseTryAgain": "Nelze přidat nový indexer, zkuste to znovu.", "UnableToLoadNotifications": "Nelze načíst oznámení", - "UpdateCheckStartupTranslocationMessage": "Aktualizaci nelze nainstalovat, protože spouštěcí složka „{0}“ je ve složce Translocation aplikace.", - "UpdateCheckUINotWritableMessage": "Aktualizaci nelze nainstalovat, protože uživatelská složka „{0}“ není zapisovatelná uživatelem „{1}“.", + "UpdateStartupTranslocationHealthCheckMessage": "Aktualizaci nelze nainstalovat, protože spouštěcí složka „{startupFolder}“ je ve složce Translocation aplikace.", + "UpdateUiNotWritableHealthCheckMessage": "Aktualizaci nelze nainstalovat, protože uživatelská složka „{uiFolder}“ není zapisovatelná uživatelem „{userName}“.", "UpdateMechanismHelpText": "Použijte vestavěný aktualizátor {appName} nebo skript", "UpdateScriptPathHelpText": "Cesta k vlastnímu skriptu, který přebírá extrahovaný balíček aktualizace a zpracovává zbytek procesu aktualizace", "Uptime": "Provozuschopnost", @@ -171,19 +171,19 @@ "UseProxy": "Použij proxy", "Username": "Uživatelské jméno", "Yesterday": "Včera", - "AutomaticSearch": "Vyhledat automaticky", - "BackupFolderHelpText": "Relativní cesty budou v adresáři AppData společnosti {appName}", + "AutomaticSearch": "Automatické vyhledávání", + "BackupFolderHelpText": "Relativní cesty budou v adresáři AppData {appName}u", "BackupIntervalHelpText": "Interval mezi automatickými zálohami", - "BackupNow": "Ihned zálohovat", + "BackupNow": "Zálohovat nyní", "BackupRetentionHelpText": "Automatické zálohy starší než doba uchovávání budou automaticky vyčištěny", - "BeforeUpdate": "Před zálohováním", + "BeforeUpdate": "Před aktualizací", "BindAddress": "Vázat adresu", - "BindAddressHelpText": "Platná IP adresa, localhost nebo '*' pro všechna rozhraní", - "BranchUpdateMechanism": "Větev používaná externím aktualizačním mechanismem", + "BindAddressHelpText": "Platná IP adresa, localhost nebo ‚*‘ pro všechna rozhraní", + "BranchUpdateMechanism": "Větev použitá externím aktualizačním mechanismem", "BypassProxyForLocalAddresses": "Obcházení proxy serveru pro místní adresy", - "DeleteIndexerProxyMessageText": "Opravdu chcete smazat značku „{0}“?", - "DeleteTag": "Smazat značku", - "IndexerProxyStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání: {0}", + "DeleteIndexerProxyMessageText": "Opravdu chcete odstranit proxy indexeru ‚{name}‘?", + "DeleteTag": "Odstranit štítek", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxy indexerů nedostupné z důvodu selhání: {indexerProxyNames}", "Name": "název", "New": "Nový", "Protocol": "Protokol", @@ -193,17 +193,17 @@ "Restart": "Restartujte", "RestartNow": "Restartovat nyní", "RestoreBackup": "Obnovit zálohu", - "RSSIsNotSupportedWithThisIndexer": "RSS není u tohoto indexeru podporováno", + "RssIsNotSupportedWithThisIndexer": "RSS není u tohoto indexeru podporováno", "Save": "Uložit", "SSLCertPasswordHelpText": "Heslo pro soubor pfx", "SSLCertPath": "Cesta certifikátu SSL", "SSLCertPathHelpText": "Cesta k souboru pfx", - "UnableToLoadBackups": "Nelze načíst zálohy", - "UnableToLoadDownloadClients": "Nelze načíst klienty pro stahování", + "BackupsLoadError": "Nelze načíst zálohy", + "DownloadClientsLoadError": "Nelze načíst klienty pro stahování", "UnableToLoadGeneralSettings": "Nelze načíst obecná nastavení", - "DeleteNotification": "Smazat oznámení", + "DeleteNotification": "Odstranit oznámení", "EnableAutomaticSearch": "Povolit automatické vyhledávání", - "EnableInteractiveSearchHelpText": "Bude použito při použití interaktivního vyhledávání", + "EnableInteractiveSearchHelpText": "Použije se při interaktivním vyhledávání", "GeneralSettings": "Obecné nastavení", "InteractiveSearch": "Interaktivní vyhledávání", "Interval": "Interval", @@ -221,52 +221,52 @@ "Restore": "Obnovit", "SettingsShowRelativeDates": "Zobrazit relativní data", "SettingsShowRelativeDatesHelpText": "Zobrazit relativní (dnes / včera / atd.) Nebo absolutní data", - "SystemTimeCheckMessage": "Systémový čas je vypnutý o více než 1 den. Naplánované úlohy nemusí fungovat správně, dokud nebude čas opraven", - "AddingTag": "Přidání značky", + "SystemTimeHealthCheckMessage": "Systémový čas je vypnutý o více než 1 den. Naplánované úlohy nemusí fungovat správně, dokud nebude čas opraven", + "AddingTag": "Přidávání štítku", "Age": "Stáří", "All": "Vše", - "AllIndexersHiddenDueToFilter": "Všechny filmy jsou skryty kvůli použitému filtru.", + "AllIndexersHiddenDueToFilter": "Všechny indexery jsou skryty kvůli použitému filtru.", "Analytics": "Analýzy", "EnableRss": "Povolit RSS", "NoChange": "Žádná změna", "Authentication": "Ověřování", - "AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k {appName}", + "AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k {appName}u", "Automatic": "Automatický", "Backup": "Záloha", "Cancel": "Zrušit", - "CertificateValidationHelpText": "Změňte, jak přísné je ověření certifikace HTTPS", + "CertificateValidationHelpText": "Změňte přísnost ověřování certifikace HTTPS", "ChangeHasNotBeenSavedYet": "Změna ještě nebyla uložena", - "Clear": "Vyčistit", + "Clear": "Vymazat", "ClientPriority": "Priorita klienta", "CloneProfile": "Klonovat profil", "Close": "Zavřít", "CouldNotConnectSignalR": "Nelze se připojit k SignalR, uživatelské rozhraní se neaktualizuje", "CustomFilters": "Vlastní filtry", - "Date": "datum", - "Dates": "Termíny", - "DBMigration": "Migrace databáze", - "Delete": "Vymazat", - "DeleteApplicationMessageText": "Opravdu chcete smazat oznámení „{0}“?", + "Date": "Datum", + "Dates": "Data", + "DatabaseMigration": "Migrace databáze", + "Delete": "Odstranit", + "DeleteApplicationMessageText": "Opravdu chcete odstranit aplikaci ‚{name}‘?", "DeleteBackup": "Odstranit zálohu", - "DeleteDownloadClient": "Odstranit staženého klienta", - "DeleteNotificationMessageText": "Opravdu chcete smazat oznámení „{0}“?", - "DeleteTagMessageText": "Opravdu chcete smazat značku „{0}“?", - "Discord": "Svár", - "DownloadClient": "Stáhnout klienta", - "DownloadClients": "Stáhnout klienty", + "DeleteDownloadClient": "Odstranit klienta pro stahování", + "DeleteNotificationMessageText": "Opravdu chcete odstranit oznámení ‚{name}‘?", + "DeleteTagMessageText": "Opravdu chcete odstranit štítek ‚{label}‘?", + "Discord": "Discord", + "DownloadClient": "Klient pro stahování", + "DownloadClients": "Klienti pro stahování", "Edit": "Upravit", - "Enable": "Umožnit", - "EnableAutomaticSearchHelpText": "Použije se, když se automatické vyhledávání provádí pomocí uživatelského rozhraní nebo {appName}", + "Enable": "Povolit", + "EnableAutomaticSearchHelpText": "Použije se při automatickém vyhledávání prostřednictvím uživatelského rozhraní nebo pomocí {appName}", "EnableInteractiveSearch": "Povolit interaktivní vyhledávání", "EnableSSL": "Povolit SSL", "EnableSslHelpText": " Vyžaduje restartování spuštěné jako správce, aby se projevilo", "Events": "Události", "EventType": "Typ události", "Exception": "Výjimka", - "ExistingTag": "Stávající značka", + "ExistingTag": "Stávající štítek", "IllRestartLater": "Restartuji později", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání po dobu delší než 6 hodin: {0}", - "IndexerStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexery nedostupné z důvodu selhání déle než 6 hodin: {indexerNames}", + "IndexerStatusUnavailableHealthCheckMessage": "Indexery nedostupné z důvodu selhání: {indexerNames}", "SettingsTimeFormat": "Časový formát", "ShowAdvanced": "Zobrazit pokročilé", "ShowSearch": "Zobrazit vyhledávání", @@ -288,7 +288,7 @@ "PortNumber": "Číslo portu", "Result": "Výsledek", "Retention": "Zadržení", - "RSS": "RSS", + "Rss": "RSS", "TagsSettingsSummary": "Podívejte se na všechny značky a na to, jak se používají. Nepoužité značky lze odstranit", "TestAll": "Vyzkoušet vše", "TestAllClients": "Vyzkoušejte všechny klienty", @@ -307,9 +307,9 @@ "UnsavedChanges": "Neuložené změny", "UpdateAutomaticallyHelpText": "Automaticky stahovat a instalovat aktualizace. Stále budete moci instalovat ze systému: Aktualizace", "NetCore": ".NET Core", - "Filters": "Filtr", - "HistoryCleanupDaysHelpText": "Nastavením na 0 zakážete automatické čištění", - "HistoryCleanupDaysHelpTextWarning": "Soubory v koši starší než vybraný počet dní budou automaticky vyčištěny", + "Filters": "Filtry", + "HistoryCleanupDaysHelpText": "Nastavte na 0 pro zakázání automatického čištění", + "HistoryCleanupDaysHelpTextWarning": "Položky historie starší než vybraný počet dní se vyčistí automaticky", "MaintenanceRelease": "Údržbové vydání: opravy chyb a další vylepšení. Další podrobnosti najdete v GitHub Commit History", "OnGrab": "Chyť", "OnHealthIssue": "K otázce zdraví", @@ -319,83 +319,325 @@ "No": "Ne", "UnableToLoadIndexers": "Nelze načíst indexery", "Yes": "Ano", - "GrabReleases": "Uchopte uvolnění", - "ApplicationLongTermStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání po dobu delší než 6 hodin: {0}", - "ApplicationLongTermStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání po dobu delší než 6 hodin", - "Ended": "Skončil", + "GrabReleases": "Získat vydání", + "ApplicationLongTermStatusCheckSingleClientMessage": "Aplikace nedostupné z důvodu selhání déle než 6 hodin: {0}", + "ApplicationLongTermStatusCheckAllClientMessage": "Všechny aplikace jsou nedostupné z důvodu selhání déle než 6 hodin", + "Ended": "Ukončeno", "LastDuration": "lastDuration", "LastExecution": "Poslední poprava", "NextExecution": "Další spuštění", "Queued": "Ve frontě", "Remove": "Odstranit", "Replace": "Nahradit", - "TheLatestVersionIsAlreadyInstalled": "Nejnovější verze aplikace {appName} je již nainstalována", + "OnLatestVersion": "Nejnovější verze aplikace {appName} je již nainstalována", "More": "Více", - "ApplyTagsHelpTextAdd": "Přidat: Přidá značky k již existujícímu seznamu", - "ApplyTagsHelpTextHowToApplyApplications": "Jak použít značky na vybrané filmy", - "DeleteSelectedDownloadClients": "Odstranit staženého klienta", - "DeleteSelectedIndexersMessageText": "Opravdu chcete odstranit indexer „{0}“?", - "DeleteSelectedApplicationsMessageText": "Opravdu chcete odstranit indexer „{0}“?", - "DeleteSelectedDownloadClientsMessageText": "Opravdu chcete odstranit indexer „{0}“?", + "ApplyTagsHelpTextAdd": "Přidat: Přidat štítky do existujícího seznamu štítků", + "ApplyTagsHelpTextHowToApplyApplications": "Jak použít štítky na vybrané aplikace", + "DeleteSelectedDownloadClients": "Odstranit klienty pro stahování", + "DeleteSelectedIndexersMessageText": "Opravdu chcete odstranit {count} vybraných indexerů?", + "DeleteSelectedApplicationsMessageText": "Opravdu chcete odstranit {count} vybraných aplikací?", + "DeleteSelectedDownloadClientsMessageText": "Opravdu chcete odstranit {count} vybraných klientů pro stahování?", "Year": "Rok", - "ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané značky", - "DownloadClientPriorityHelpText": "Upřednostněte více klientů pro stahování. Round-Robin se používá pro klienty se stejnou prioritou.", - "ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané indexátory", - "ApplyTagsHelpTextReplace": "Nahradit: Nahradit značky zadanými značkami (zadáním žádné značky vymažete všechny značky)", + "ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané štítky", + "DownloadClientPriorityHelpText": "Upřednostněte více klientů pro stahování. Pro klienty se stejnou prioritou se používá funkce Round-Robin.", + "ApplyTagsHelpTextHowToApplyIndexers": "Jak použít štítky na vybrané indexery", + "ApplyTagsHelpTextReplace": "Nahradit: Nahradit štítky zadanými štítky (prázdné pole vymaže všechny štítky)", "Track": "Stopa", - "Genre": "Žánry", + "Genre": "Žánr", "ConnectionLostReconnect": "{appName} se pokusí připojit automaticky, nebo můžete kliknout na tlačítko znovunačtení níže.", "RecentChanges": "Nedávné změny", "WhatsNew": "Co je nového?", - "DeleteAppProfileMessageText": "Opravdu chcete smazat kvalitní profil {0}", - "ConnectionLostToBackend": "{appName} ztratila spojení s backendem a pro obnovení funkčnosti bude třeba ji znovu načíst.", + "DeleteAppProfileMessageText": "Opravdu chcete odstranit profil aplikace ‚{name}‘?", + "ConnectionLostToBackend": "{appName} ztratil spojení s backendem a pro obnovení funkčnosti bude potřeba ho znovu načíst.", "minutes": "Minut", "ApplicationURL": "URL aplikace", - "ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základní adresy URL", + "ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základu URL", "ApplyChanges": "Použít změny", - "ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {0} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru", + "ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {length} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru", "AppUpdated": "{appName} aktualizován", - "AddDownloadClientImplementation": "Přidat klienta pro stahování - {implementationName}", - "AuthenticationRequired": "Vyžadované ověření", - "AuthenticationRequiredHelpText": "Změnit, pro které požadavky je vyžadováno ověření. Pokud nerozumíte rizikům, neměňte je.", + "AddDownloadClientImplementation": "Přidat klienta pro stahování – {implementationName}", + "AuthenticationRequired": "Vyžadováno ověření", + "AuthenticationRequiredHelpText": "Změnit, pro které požadavky je vyžadováno ověření. Neměňte, pokud nerozumíte rizikům.", "AddCustomFilter": "Přidat vlastní filtr", "AddConnection": "Přidat spojení", - "AddConnectionImplementation": "Přidat spojení - {implementationName}", - "AddIndexerImplementation": "Přidat indexátor - {implementationName}", + "AddConnectionImplementation": "Přidat spojení – {implementationName}", + "AddIndexerImplementation": "Přidat indexer – {implementationName}", "Publisher": "Vydavatel", "Categories": "Kategorie", "Notification": "Oznámení", - "AddApplicationImplementation": "Přidat spojení - {implementationName}", - "AddIndexerProxyImplementation": "Přidat indexátor - {implementationName}", - "Artist": "umělec", - "EditIndexerImplementation": "Přidat indexátor - {implementationName}", - "Episode": "epizoda", - "NotificationStatusAllClientHealthCheckMessage": "Všechny seznamy nejsou k dispozici z důvodu selhání", - "NotificationStatusSingleClientHealthCheckMessage": "Seznamy nejsou k dispozici z důvodu selhání: {0}", + "AddApplicationImplementation": "Přidat aplikaci – {implementationName}", + "AddIndexerProxyImplementation": "Přidat proxy server indexeru – {implementationName}", + "Artist": "Umělec", + "EditIndexerImplementation": "Upravit indexer - {implementationName}", + "Episode": "Epizoda", + "NotificationStatusAllClientHealthCheckMessage": "Všechna oznámení jsou nedostupná z důvodu selhání", + "NotificationStatusSingleClientHealthCheckMessage": "Oznámení nedostupná z důvodu selhání: {notificationNames}", "Application": "Aplikace", - "AppUpdatedVersion": "{appName} byla aktualizována na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}.", + "AppUpdatedVersion": "{appName} byl aktualizován na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}", "Encoding": "Kódování", "Notifications": "Oznámení", "Season": "Řada", "Theme": "Motiv", "Label": "Etiketa", - "Album": "album", + "Album": "Album", "Applications": "Aplikace", "Connect": "Oznámení", - "EditConnectionImplementation": "Přidat spojení - {implementationName}", - "EditDownloadClientImplementation": "Přidat klienta pro stahování - {implementationName}", + "EditConnectionImplementation": "Upravit připojení - {implementationName}", + "EditDownloadClientImplementation": "Upravit klienta pro stahování - {implementationName}", "AuthForm": "Formuláře (přihlašovací stránka)", "Clone": "Klonovat", "DefaultNameCopiedProfile": "{name} - Kopírovat", "DisabledForLocalAddresses": "Zakázáno pro místní adresy", - "EditApplicationImplementation": "Přidat spojení - {implementationName}", + "EditApplicationImplementation": "Upravit aplikaci - {implementationName}", "None": "Žádný", "ResetAPIKeyMessageText": "Opravdu chcete resetovat klíč API?", "Database": "Databáze", - "CountDownloadClientsSelected": "{count} vybraných klientů ke stahování", - "CountIndexersSelected": "{count} vybraných indexátorů", - "EditIndexerProxyImplementation": "Přidat indexátor - {implementationName}", + "CountDownloadClientsSelected": "{count} vybraných klientů pro stahování", + "CountIndexersSelected": "{count} vybraných indexerů", + "EditIndexerProxyImplementation": "Upravit proxy indexeru - {implementationName}", "AuthBasic": "Základní (vyskakovací okno prohlížeče)", - "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní {appName} povolení ověření. Ověřování z místních adres můžete volitelně zakázat.", - "RestartProwlarr": "Restartujte {appName}" + "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní {appName}, aby bylo povoleno ověřování. Volitelně můžete zakázat ověřování z místních adres.", + "RestartProwlarr": "Restartujte {appName}", + "Duration": "Trvání", + "EditSelectedDownloadClients": "Upravit vybrané klienty pro stahování", + "EditSelectedIndexers": "Upravit vybrané indexery", + "AuthenticationMethod": "Metoda ověřování", + "AuthenticationRequiredPasswordHelpTextWarning": "Zadejte nové heslo", + "AuthenticationRequiredUsernameHelpTextWarning": "Zadejte nové uživatelské jméno", + "AuthenticationMethodHelpTextWarning": "Vyberte platnou metodu ověřování", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrďte nové heslo", + "days": "dnů", + "Id": "ID", + "CountApplicationsSelected": "{count} vybraných aplikací", + "IndexerHDBitsSettingsCodecs": "Kodeky", + "IndexerHDBitsSettingsMediums": "Střední", + "Directory": "Adresář", + "CustomFilter": "Vlastní filtr", + "ProxyValidationBadRequest": "Nepodařilo se otestovat proxy. StatusCode: {statusCode}", + "Default": "Výchozí", + "GrabRelease": "Získat vydání", + "Category": "Kategorie", + "BlackholeFolderHelpText": "Složka, do které {appName} uloží soubor {extension}", + "DownloadClientSettingsUrlBaseHelpText": "Přidá předponu k url {clientName}, například {url}", + "Any": "Jakákoliv", + "BuiltIn": "Vestavěný", + "Script": "Skript", + "PublishedDate": "Datum zveřejnění", + "AllSearchResultsHiddenByFilter": "Všechny výsledky vyhledávání jsou skryty použitým filtrem.", + "DockerUpdater": "Aktualizujte kontejner dockeru, abyste získali aktualizaci", + "Download": "Stažení", + "ErrorRestoringBackup": "Chyba při obnovování zálohy", + "ExternalUpdater": "{appName} je nakonfigurován pro použití externího aktualizačního mechanismu", + "FailedToFetchUpdates": "Nepodařilo se načíst aktualizace", + "NoEventsFound": "Nebyly nalezeny žádné události", + "RestartReloadNote": "Poznámka: {appName} se během procesu obnovy automaticky restartuje a znovu načte uživatelské rozhraní.", + "UpdateAppDirectlyLoadError": "{appName} nelze aktualizovat přímo,", + "AptUpdater": "K instalaci aktualizace používat apt", + "InstallLatest": "Nainstalujte nejnovější", + "Stats": "Postavení", + "CurrentlyInstalled": "Aktuálně nainstalováno", + "Mixed": "Pevný", + "ActiveIndexers": "Aktivní indexery", + "ActiveApps": "Aktivní aplikace", + "AppSettingsSummary": "Aplikace a nastavení pro konfiguraci interakce {appName}u s vašimi programy PVR", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Pokud je torrent blokován pomocí hash, nemusí být u některých indexerů správně odmítnut během RSS/vyhledávání. Povolení této funkce umožní jeho odmítnutí po zachycení torrentu, ale před jeho odesláním klientovi.", + "ApplicationsLoadError": "Nelze načíst seznam aplikací", + "AppProfileInUse": "Používaný profil aplikace", + "AppsMinimumSeedersHelpText": "Minimální počet seederů požadovaných aplikacemi pro indexer, výchozí hodnota synchronizačního profilu je prázdná", + "AverageGrabs": "Průměrné získání", + "AverageQueries": "Průměrné dotazy", + "AdvancedSettingsShownClickToHide": "Rozšířená nastavení jsou zobrazená, klikněte pro skrytí", + "AdvancedSettingsHiddenClickToShow": "Rozšířená nastavení jsou skrytá, klikněte pro zobrazení", + "AppsMinimumSeeders": "Minimální počet seederů aplikací", + "AddNewIndexer": "Přidat nový indexer", + "AddToDownloadClient": "Přidat vydání do klienta pro stahování", + "AddIndexerProxy": "Přidat proxy server indexeru", + "AppProfileSelectHelpText": "Profily aplikace slouží k ovládání nastavení RSS, automatického vyhledávání a interaktivního vyhledávání při synchronizaci aplikace", + "BookSearch": "Vyhledávání knihy", + "ClearHistory": "Vymazat historii", + "Auth": "Ověřování", + "ConnectSettingsSummary": "Oznámení a vlastní skripty", + "AreYouSureYouWantToDeleteIndexer": "Opravdu chcete odstranit ‚{name}‘ z {appName}u?", + "AuthQueries": "Ověřovací dotazy", + "CountIndexersAvailable": "{count} dostupných indexerů", + "ApplicationTagsHelpText": "Synchronizovat s touto aplikací indexery, které mají jeden nebo více shodných štítků. Pokud zde nejsou uvedeny žádné štítky, nebude synchronizace žádných indexerů znemožněna kvůli jejich štítkům.", + "ApplicationTagsHelpTextWarning": "Štítky je potřeba používat opatrně, mohou mít nechtěné účinky. Aplikace se štítkem se bude synchronizovat pouze s indexery se stejným štítkem.", + "BasicSearch": "Základní vyhledávání", + "ClearHistoryMessageText": "Opravdu chcete vymazat celou historii {appName}u?", + "AddDownloadClientToProwlarr": "Přidání klienta pro stahování umožňuje {appName} odesílat vydání přímo z uživatelského rozhraní při ručním vyhledávání.", + "AddRemoveOnly": "Pouze přidat a odebrat", + "AudioSearch": "Vyhledávání audia", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Synchronizovat odmítnuté blokované hashe torrentů při získávání", + "Apps": "Aplikace", + "ClickToChangeQueryOptions": "Kliknutím změníte možnosti dotazu", + "Author": "Autor", + "AverageResponseTimesMs": "Průměrné doby odezvy indexerů (ms)", + "Book": "Kniha", + "BookSearchTypes": "Typy vyhledávání knihy", + "AddApplication": "Přidat aplikaci", + "AddSyncProfile": "Přidat synchronizační profil", + "AddedToDownloadClient": "Vydání přidáno do klienta", + "AddCategory": "Přidat kategorii", + "AreYouSureYouWantToDeleteCategory": "Opravdu chcete odstranit namapovanou kategorii?", + "DownloadClientRTorrentSettingsUrlPath": "Cesta URL", + "DefaultCategory": "Výchozí kategorie", + "DownloadClientFloodSettingsUrlBaseHelpText": "Přidá předponu do Flood API, například {url}", + "DownloadClientFreeboxSettingsApiUrl": "URL API", + "DownloadClientSettingsInitialState": "Počáteční stav", + "DownloadClientSettingsPriorityItemHelpText": "Priorita použitá při získávání položek", + "FailedToFetchSettings": "Nepodařilo se načíst nastavení", + "GrabTitle": "Získat název", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Tato volba vyžaduje NzbGet verze alespoň 16.0", + "EnableRssHelpText": "Povolit kanál RSS pro indexer", + "DeleteApplication": "Odstranit aplikaci", + "DeleteSelectedApplications": "Odstranit vybrané aplikace", + "DeleteSelectedIndexers": "Odstranit vybrané indexery", + "DevelopmentSettings": "Nastavení pro vývoj", + "DisabledUntil": "Zakázáno do", + "DownloadClientCategory": "Kategorie klienta pro stahování", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Přidá předponu do url adresy json deluge, viz {url}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Volitelná sdílená složka pro stahování, ponechte prázdné pro použití výchozího umístění Download Station", + "DownloadClientFloodSettingsAdditionalTags": "Další štítky", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Přidá vlastnosti médií jako štítky. Nápovědy jsou příklady.", + "DownloadClientFloodSettingsTagsHelpText": "Počáteční štítky stahování. Aby bylo stahování rozpoznáno, musí mít všechny počáteční štítky. Tím se zabrání konfliktům s nesouvisejícími stahováními.", + "DownloadClientFreeboxSettingsAppId": "ID aplikace", + "DownloadClientFreeboxSettingsAppToken": "Token aplikace", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikace získaný při vytváření přístupu k Freebox API (tj. ‚app_token‘)", + "DownloadClientFreeboxSettingsHostHelpText": "Název hostitele nebo IP adresa hostitele Freeboxu, výchozí hodnota je ‚{url}‘ (funguje pouze ve stejné síti)", + "DownloadClientFreeboxSettingsPortHelpText": "Port použitý pro přístup k rozhraní Freeboxu, výchozí hodnota je ‚{port}‘", + "DownloadClientPneumaticSettingsNzbFolder": "Složka Nzb", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Tato složka bude muset být dostupná z XBMC", + "DownloadClientQbittorrentSettingsContentLayout": "Rozvržení obsahu", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Stahovat v postupném pořadí (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Postupné pořadí", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Povolení přidá torrenty a magnety do rTorrentu v zastaveném stavu. To může způsobit poškození souborů magnet.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Volitelné umístění pro stahování, ponechte prázdné pro použití výchozího umístění rTorrentu", + "DownloadClientSettingsAddPaused": "Přidat pozastavené", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Výchozí záložní kategorie, pokud pro vydání neexistuje žádná namapovaná kategorie. Přidáním kategorie specifické pro {appName} se zabrání konfliktům s nesouvisejícími stahováními, která nejsou {appName}. Použití kategorie je nepovinné, ale důrazně se doporučuje. Vytvoří podadresář [kategorie] ve výstupním adresáři.", + "DownloadClientSettingsUseSslHelpText": "Při připojení k {clientName} použít zabezpečené připojení", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Volitelné umístění pro stahování, ponechte prázdné pro použití výchozího umístění Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Přidá předponu k url {clientName} rpc, např. {url}, výchozí hodnota je ‚{defaultUrl}‘", + "DownloadClientsSettingsSummary": "Konfigurace klientů pro stahování pro integraci do vyhledávání v uživatelském rozhraní {appName}", + "ElapsedTime": "Uplynulý čas", + "EnableIndexer": "Povolit indexer", + "External": "Externí", + "FullSync": "Úplná synchronizace", + "HealthMessagesInfoBox": "Další informace o příčině těchto zpráv o kontrole zdraví najdete kliknutím na odkaz wiki (ikona knihy) na konci řádku nebo kontrolou [logů]({link}). Pokud máte potíže s interpretací těchto zpráv, můžete se obrátit na naši podporu, a to na níže uvedených odkazech.", + "Implementation": "Implementace", + "DeleteClientCategory": "Odstranit kategorii klienta pro stahování", + "DownloadClientRTorrentSettingsAddStopped": "Přidat zastavené", + "DeleteIndexerProxy": "Odstranit proxy indexerů", + "Description": "Popis", + "IncludeManualGrabsHelpText": "Včetně ručních získání provedených v {appName}", + "GoToApplication": "Přejít na aplikaci", + "DownloadClientAriaSettingsDirectoryHelpText": "Volitelné umístění pro stahování, ponechte prázdné pro použití výchozího umístění Aria2", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Soubory .strm v této složce budou importovány pomocí drone", + "Destination": "Cíl", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Stahovat nejprve první a poslední kusy (qBittorrent 4.1.0+)", + "DeleteSelectedIndexer": "Odstranit vybraný indexer", + "DownloadClientPneumaticSettingsStrmFolder": "Složka Strm", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Nejprve první a poslední", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Definujte základní adresu URL Freebox API s verzí API, např. ‚{url}‘, výchozí hodnota je ‚{defaultApiUrl}‘", + "HistoryCleanup": "Vyčištění historie", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID aplikace zadané při vytváření přístupu k Freebox API (tj. ‚app_id‘)", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Zda použít rozvržení obsahu nakonfigurované v qBittorrentu, původní rozvržení z torrentu nebo vždy vytvořit podsložku (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Počáteční stav torrentů přidaných do qBittorrentu. Pamatujte, že vynucené torrenty nedodržují omezení týkající se seedů", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Používat zabezpečené připojení. Viz Možnosti -> WebUI -> Webové uživatelské rozhraní -> ‚Použít HTTPS místo HTTP‘ v qBittorrentu.", + "FilterPlaceHolder": "Hledat indexery", + "FoundCountReleases": "Nalezeno {itemCount} vydání", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Cesta ke koncovému bodu XMLRPC, viz {url}. Při použití ruTorrentu je to obvykle RPC2 nebo [cesta k ruTorrentu]{url2}.", + "DownloadClientSettingsDefaultCategoryHelpText": "Výchozí záložní kategorie, pokud pro vydání neexistuje žádná namapovaná kategorie. Přidáním kategorie specifické pro {appName} se zabrání konfliktům s nesouvisejícími stahováními, která nejsou {appName}. Použití kategorie je nepovinné, ale důrazně se doporučuje.", + "DownloadClientSettingsInitialStateHelpText": "Počáteční stav pro torrenty přidané do {clientName}", + "EditCategory": "Upravit kategorii", + "HistoryDetails": "Podrobnosti o historii", + "Donate": "Darovat", + "DownloadClientSettingsDestinationHelpText": "Ručně určuje cíl stahování, pro použití výchozího nastavení nechte prázdné", + "EnabledRedirected": "Povoleno, Přesměrováno", + "EditSyncProfile": "Upravit profil synchronizace", + "DeleteAppProfile": "Odstranit profil aplikace", + "IndexerSettingsAppsMinimumSeeders": "Minimální počet seederů aplikací", + "UsenetBlackholeNzbFolder": "Složka Nzb", + "SearchIndexers": "Hledat indexery", + "IndexerSettingsAppsMinimumSeedersHelpText": "Minimální počet seederů požadovaných aplikacemi pro indexer, výchozí hodnota synchronizačního profilu je prázdná", + "IndexerProxy": "Proxy indexeru", + "IndexerBeyondHDSettingsRssKeyHelpText": "Klíč RSS ze stránky (Naleznete v Moje zabezpečení => Klíč RSS)", + "IndexerHDBitsSettingsCodecsHelpText": "Pokud není zadáno, použijí se všechny možnosti.", + "IndexerHDBitsSettingsUsernameHelpText": "Uživatelské jméno stránky", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Rozhraní API v tomto indexeru mohou používat pouze hodnosti člen a vyšší.", + "IndexerBeyondHDSettingsApiKeyHelpText": "Klíč API ze stránky (Naleznete v Moje zabezpečení => Klíč API)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerProxies": "Proxy indexeru", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerHistoryLoadError": "Chyba při načítání historie indexeru", + "IndexerId": "ID indexeru", + "IndexerNoDefinitionCheckHealthCheckMessage": "Indexery nemají žádnou definici a nebudou fungovat: {indexerNames}. Odeberte je a (nebo) znovu přidejte do {appName}.", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Vyloučit vydání SCENE z výsledků", + "IndexerAlreadySetup": "Alespoň jedna instance indexeru je již nastavena", + "IndexerAvistazSettingsPasswordHelpText": "Heslo stránky", + "IndexerAvistazSettingsPidHelpText": "PID ze stránky Můj účet nebo Můj profil", + "IndexerAvistazSettingsUsernameHelpText": "Uživatelské jméno stránky", + "IndexerBeyondHDSettingsLimitedOnly": "Pouze omezené", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Hledat pouze freeleech (Omezené nahrávání)", + "IndexerCategories": "Kategorie indexeru", + "IndexerDisabled": "Indexer zakázán", + "IndexerDownloadClientHealthCheckMessage": "Indexery s neplatnými klienty pro stahování: {indexerNames}.", + "IndexerDownloadClientHelpText": "Určete, který klient pro stahování se použije pro grabování v rámci {appName} z tohoto indexeru", + "IndexerFailureRate": "Míra selhání indexeru", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerFileListSettingsPasskeyHelpText": "Přístupový klíč stránky (Jedná se o alfanumerický řetězec v url adrese trackeru zobrazené v klientovi pro stahování)", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Klíč API ze stránky (Naleznete v Nastavení => Nastavení přístupu)", + "IndexerGazelleGamesSettingsSearchGroupNames": "Hledat názvy skupin", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Zobrazit pouze freeleech vydání", + "IndexerHDBitsSettingsOriginsHelpText": "Pokud není zadáno, použijí se všechny možnosti.", + "IndexerHDBitsSettingsUseFilenames": "Použít názvy souborů", + "IndexerHealthCheckNoIndexers": "Nejsou povoleny žádné indexery, {appName} nevrátí výsledky vyhledávání", + "IndexerIPTorrentsSettingsCookieUserAgent": "Uživatelský agent cookie", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Uživatelský agent přidružený cookie použitý z prohlížeče", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerNebulanceSettingsApiKeyHelpText": "Klíč API z Nastavení uživatele > Klíče API. Klíč musí mít oprávnění Seznam a Stáhnout", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Dodatečné parametry Newznab", + "IndexerNewznabSettingsApiKeyHelpText": "Klíč API stránky", + "IndexerObsoleteCheckMessage": "Indexery jsou zastaralé nebo byly aktualizovány: {0}. Odeberte je a (nebo) znovu přidejte do {appName}", + "IndexerOrpheusSettingsApiKeyHelpText": "Klíč API ze stránky (Naleznete v Nastavení => Nastavení přístupu)", + "IndexerPassThePopcornSettingsApiUserHelpText": "Tato nastavení naleznete v nastavení zabezpečení PassThePopcorn (Upravit profil > Zabezpečení).", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerRedactedSettingsApiKeyHelpText": "Klíč API ze stránky (Naleznete v Nastavení => Nastavení přístupu)", + "IndexerRss": "RSS indexeru", + "LastFailure": "Poslední selhání", + "IndexerSettingsAdditionalParameters": "Dodatečné parametry", + "IndexerSettingsApiPath": "Cesta k API", + "IndexerSettingsApiUser": "Uživatel API", + "IndexerAuth": "Ověření indexeru", + "IndexerInfo": "Informace o indexeru", + "IndexerName": "Název indexeru", + "IndexerDetails": "Podrobnosti indexeru", + "IndexerHDBitsSettingsPasskeyHelpText": "Přístupový klíč z Podrobnosti o uživateli", + "IndexerQuery": "Dotaz na indexer", + "IndexerAlphaRatioSettingsExcludeScene": "Vyloučit SCENE", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "IndexerBeyondHDSettingsSearchTypes": "Hledat typy", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Vyberte typy vydání, které vás zajímají. Pokud není vybrán žádný, použijí se všechny možnosti.", + "IndexerFileListSettingsUsernameHelpText": "Uživatelské jméno stránky", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Musí mít oprávnění Uživatel a Torrenty", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Hledat vydání podle názvů skupin", + "IndexerHDBitsSettingsMediumsHelpText": "Pokud není zadáno, použijí se všechny možnosti.", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Zaškrtněte tuto možnost, pokud chcete používat názvy souborů torrentů jako názvy vydání", + "IndexerNewznabSettingsVipExpirationHelpText": "Zadejte datum (rrrr-mm-dd) pro vypršení VIP nebo prázdné, {appName} bude upozorňovat 1 týden před vypršením VIP", + "IndexerNzbIndexSettingsApiKeyHelpText": "Klíč API stránky", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Klíč API stránky", + "IndexerMTeamTpSettingsApiKeyHelpText": "Klíč API ze stránky (Naleznete v Uživatelský ovládací panel => Zabezpečení => Laboratoř)", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Pouze Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Hledat pouze vydání Golden Popcorn", + "IndexerSettingsApiPathHelpText": "Cesta k api, obvykle {url}", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Hledat pouze freeleech vydání", + "InitialFailure": "Úvodní selhání", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Klíč API stránky", + "SearchTypes": "Hledat typy", + "NotificationTriggersHelpText": "Vyber, které události mají vyvolat toto upozornění", + "IndexerSettingsBaseUrl": "Základní URL", + "DownloadClientUTorrentProviderMessage": "uTorrent má historii zahrnování kryptoměnových těžařů, malwaru a reklam, důrazně vám doporučujeme zvolit jiného klienta.", + "IndexerSettingsBaseUrlHelpText": "Vyberte, jakou základní URL bude {appName} používat pro požadavky na web", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Doba, po kterou by měl být balíček (sezóna nebo diskografie) torrentu seedován před zastavením, prázdné pole znamená výchozí nastavení aplikace", + "PackSeedTimeHelpText": "Doba, po kterou by měl být balíček (sezóna nebo diskografie) torrentu seedován před zastavením, prázdné pole znamená výchozí nastavení aplikace" } diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 74b19207d..1a118a8af 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -3,12 +3,12 @@ "Language": "Sprog", "KeyboardShortcuts": "Keyboard Genveje", "Info": "Information", - "IndexerStatusCheckSingleClientMessage": "Indexere utilgængelige på grund af fejl: {0}", - "IndexerStatusCheckAllClientMessage": "Alle indeksører er utilgængelige på grund af fejl", + "IndexerStatusUnavailableHealthCheckMessage": "Indexere utilgængelige på grund af fejl: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Alle indeksører er utilgængelige på grund af fejl", "Indexers": "Indexere", "History": "Historie", "HideAdvanced": "Gemt Avancerede", - "HealthNoIssues": "Ingen problemer med din konfiguration", + "NoIssuesWithYourConfiguration": "Ingen problemer med din konfiguration", "Health": "Helbred", "Grabbed": "Grebet", "GeneralSettingsSummary": "Port, SSL, brugernavn/adgangskode, proxy, analyse og opdateringer", @@ -21,8 +21,8 @@ "Events": "Begivenheder", "Error": "Fejl", "Edit": "Rediger", - "DownloadClientStatusCheckSingleClientMessage": "Download klienter er ikke tilgængelige på grund af fejl: {0}", - "DownloadClientStatusCheckAllClientMessage": "Alle download klienter er utilgængelige på grund af fejl", + "DownloadClientStatusSingleClientHealthCheckMessage": "Download klienter er ikke tilgængelige på grund af fejl: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Alle download klienter er utilgængelige på grund af fejl", "DownloadClientsSettingsSummary": "Indstilling af downloadklienter til brug for {appName}s søgefunktion", "DownloadClients": "Download Klienter", "DownloadClient": "Download Klient", @@ -33,7 +33,7 @@ "CustomFilters": "Bruger Tilpassede Filtere", "ConnectSettingsSummary": "Notifikationer, forbindelser til medieservere/-afspillere og brugerdefinerede scripts", "Connections": "Forbindelser", - "ConnectionLost": "Forbindelse Mistet", + "ConnectionLost": "Forbindelse mistet", "Connect": "Notifikationer", "Component": "Komponent", "Columns": "Kolonner", @@ -53,11 +53,11 @@ "Disabled": "deaktiveret", "Add": "Tilføj", "AddDownloadClient": "Tilføj downloadklient", - "DBMigration": "DB Migration", + "DatabaseMigration": "DB Migration", "MIA": "MIA", "ResetAPIKey": "Nulstil API-nøgle", "SettingsTimeFormat": "Tidsformat", - "SystemTimeCheckMessage": "Systemtiden er slukket mere end 1 dag. Planlagte opgaver kører muligvis ikke korrekt, før tiden er rettet", + "SystemTimeHealthCheckMessage": "Systemtiden er slukket mere end 1 dag. Planlagte opgaver kører muligvis ikke korrekt, før tiden er rettet", "UnsavedChanges": "Ugemte ændringer", "Updates": "Opdateringer", "MoreInfo": "Mere info", @@ -78,9 +78,9 @@ "UILanguage": "UI-sprog", "Custom": "Brugerdefinerede", "TagsSettingsSummary": "Se alle tags og hvordan de bruges. Ubrugte tags kan fjernes", - "Test": "Prøve", - "TestAll": "Test alle", - "TestAllClients": "Test alle klienter", + "Test": "Afprøv", + "TestAll": "Afprøv alle", + "TestAllClients": "Afprøv alle klienter", "Type": "Type", "UnableToAddANewIndexerProxyPleaseTryAgain": "Kunne ikke tilføje en ny indekser. Prøv igen.", "UnableToAddANewNotificationPleaseTryAgain": "Kan ikke tilføje en ny underretning, prøv igen.", @@ -92,7 +92,7 @@ "SetTags": "Indstil tags", "YesCancel": "Ja, Annuller", "AcceptConfirmationModal": "Accepter bekræftelsesmodal", - "AddIndexer": "Tilføj indeksør", + "AddIndexer": "Tilføj indekser", "AnalyticsEnabledHelpText": "Send anonym brugs- og fejlinformation til {appName}s servere. Dette inkluderer information om din browser, hvilke af {appName}s WebUI-sider du bruger, fejlrapportering, samt OS- og runtime-version. Vi bruger disse oplysninger til at prioritere funktioner og fejlrettelser.", "Backups": "Sikkerhedskopier", "BypassProxyForLocalAddresses": "Bypass-proxy til lokale adresser", @@ -101,12 +101,12 @@ "CloseCurrentModal": "Luk Nuværende Modal", "CouldNotConnectSignalR": "Kunne ikke oprette forbindelse til SignalR, UI opdateres ikke", "DeleteApplicationMessageText": "Er du sikker på, at du vil slette underretningen '{0}'?", - "DeleteDownloadClientMessageText": "Er du sikker på, at du vil slette downloadklienten '{0}'?", + "DeleteDownloadClientMessageText": "Er du sikker på, at du vil fjerne downloadklienten »{name}«?", "DeleteIndexerProxyMessageText": "Er du sikker på, at du vil slette tagget '{0}'?", "DeleteNotification": "Slet underretning", - "DeleteNotificationMessageText": "Er du sikker på, at du vil slette underretningen '{0}'?", + "DeleteNotificationMessageText": "Er du sikker på, at du vil slette notifikationen »{name}«?", "DeleteTag": "Slet tag", - "DeleteTagMessageText": "Er du sikker på, at du vil slette tagget '{0}'?", + "DeleteTagMessageText": "Er du sikker på, at du vil slette etiketten »{label}«?", "Discord": "Discord", "Docker": "Docker", "Donations": "Donationer", @@ -135,12 +135,12 @@ "IllRestartLater": "Jeg genstarter senere", "IncludeHealthWarningsHelpText": "Inkluder sundhedsadvarsler", "Indexer": "Indekser", - "IndexerLongTermStatusCheckAllClientMessage": "Alle indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer", - "IndexerLongTermStatusCheckSingleClientMessage": "Indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alle indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer: {indexerNames}", "IndexerPriority": "Indekseringsprioritet", "IndexerPriorityHelpText": "Indekseringsprioritet fra 1 (højest) til 50 (lavest). Standard: 25.", - "IndexerProxyStatusCheckAllClientMessage": "Alle indexere er utilgængelige på grund af fejl", - "IndexerProxyStatusCheckSingleClientMessage": "Indexere utilgængelige på grund af fejl: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Alle indexere er utilgængelige på grund af fejl", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Indexere utilgængelige på grund af fejl: {indexerProxyNames}", "LaunchBrowserHelpText": " Åbn en webbrowser, og naviger til {appName}-hjemmesiden ved start af appen.", "Logging": "Logning", "LogLevel": "Logniveau", @@ -173,9 +173,9 @@ "Priority": "Prioritet", "Protocol": "Protokol", "ProxyBypassFilterHelpText": "Brug ',' som en separator og '*.' som et jokertegn for underdomæner", - "ProxyCheckBadRequestMessage": "Kunne ikke teste proxy. Statuskode: {0}", - "ProxyCheckFailedToTestMessage": "Kunne ikke teste proxy: {0}", - "ProxyCheckResolveIpMessage": "Mislykkedes at løse IP-adressen til den konfigurerede proxyhost {0}", + "ProxyBadRequestHealthCheckMessage": "Kunne ikke teste proxy. Statuskode: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Kunne ikke teste proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Mislykkedes at løse IP-adressen til den konfigurerede proxyhost {proxyHostName}", "ProxyPasswordHelpText": "Du skal kun indtaste et brugernavn og en adgangskode, hvis der kræves et. Lad dem være tomme ellers.", "ProxyUsernameHelpText": "Du skal kun indtaste et brugernavn og en adgangskode, hvis der kræves et. Lad dem være tomme ellers.", "ReadTheWikiForMoreInformation": "Læs Wiki for mere information", @@ -232,15 +232,15 @@ "UnableToAddANewAppProfilePleaseTryAgain": "Kan ikke tilføje en ny kvalitetsprofil, prøv igen.", "UnableToAddANewDownloadClientPleaseTryAgain": "Kunne ikke tilføje en ny downloadklient. Prøv igen.", "UnableToAddANewIndexerPleaseTryAgain": "Kunne ikke tilføje en ny indekser. Prøv igen.", - "UnableToLoadBackups": "Kunne ikke indlæse sikkerhedskopier", + "BackupsLoadError": "Kunne ikke indlæse sikkerhedskopier", "UnableToLoadGeneralSettings": "Kan ikke indlæse generelle indstillinger", "UnableToLoadNotifications": "Kunne ikke indlæse meddelelser", "UnableToLoadTags": "Kan ikke indlæse tags", "UnableToLoadUISettings": "UI-indstillingerne kunne ikke indlæses", "UnselectAll": "Fravælg alle", "UpdateAutomaticallyHelpText": "Download og installer opdateringer automatisk. Du kan stadig installere fra System: Updates", - "UpdateCheckStartupNotWritableMessage": "Kan ikke installere opdatering, fordi startmappen '{0}' ikke kan skrives af brugeren '{1}'.", - "UpdateCheckUINotWritableMessage": "Kan ikke installere opdatering, fordi brugergrænsefladen \"{0}\" ikke kan skrives af brugeren \"{1}\".", + "UpdateStartupNotWritableHealthCheckMessage": "Kan ikke installere opdatering, fordi startmappen '{startupFolder}' ikke kan skrives af brugeren '{userName}'.", + "UpdateUiNotWritableHealthCheckMessage": "Kan ikke installere opdatering, fordi brugergrænsefladen \"{uiFolder}\" ikke kan skrives af brugeren \"{userName}\".", "UpdateScriptPathHelpText": "Sti til et brugerdefineret script, der tager en udpakket opdateringspakke og håndterer resten af opdateringsprocessen", "Uptime": "Oppetid", "URLBase": "URL-base", @@ -254,8 +254,8 @@ "ReleaseStatus": "Frigør status", "Restore": "Gendan", "RestoreBackup": "Gendan sikkerhedskopi", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS understøttes ikke med denne indekseringsenhed", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS understøttes ikke med denne indekseringsenhed", "Save": "Gemme", "Scheduled": "Planlagt", "ScriptPath": "Script sti", @@ -264,9 +264,9 @@ "SSLPort": "SSL-port", "StartupDirectory": "Startmappe", "Status": "Status", - "UnableToLoadDownloadClients": "Kunne ikke indlæse downloadklienter", - "UpdateCheckStartupTranslocationMessage": "Kan ikke installere opdatering, fordi startmappen '{0}' er i en App Translocation-mappe.", - "UpdateMechanismHelpText": "Brug den indbyggede opdateringsfunktion eller et script", + "DownloadClientsLoadError": "Kunne ikke indlæse downloadklienter", + "UpdateStartupTranslocationHealthCheckMessage": "Kan ikke installere opdatering, fordi startmappen '{startupFolder}' er i en App Translocation-mappe.", + "UpdateMechanismHelpText": "Brug {appName}s indbyggede opdateringsfunktion eller et script", "View": "Udsigt", "Warn": "Advare", "AddingTag": "Tilføjer tag", @@ -302,12 +302,12 @@ "InteractiveSearch": "Interaktiv søgning", "LogFiles": "Logfiler", "ApiKey": "API-nøgle", - "AppDataDirectory": "AppData-bibliotek", + "AppDataDirectory": "AppData-mappe", "CertificateValidationHelpText": "Skift, hvor streng HTTPS-certificering er", "ChangeHasNotBeenSavedYet": "Ændring er endnu ikke gemt", "ConnectSettings": "Forbind indstillinger", "DeleteBackup": "Slet sikkerhedskopi", - "DeleteBackupMessageText": "Er du sikker på, at du vil slette sikkerhedskopien '{0}'?", + "DeleteBackupMessageText": "Er du sikker på, at du vil slette sikkerhedskopien »{name}«?", "DeleteDownloadClient": "Slet Download Client", "MaintenanceRelease": "Vedligeholdelsesfrigivelse: fejlrettelser og andre forbedringer. Se Github Commit History for flere detaljer", "Filters": "Filter", @@ -315,7 +315,7 @@ "HistoryCleanupDaysHelpTextWarning": "Filer i papirkurven, der er ældre end det valgte antal dage, renses automatisk", "OnGrab": "ved hentning", "OnHealthIssue": "Om sundhedsspørgsmål", - "TestAllIndexers": "Test alle indeksører", + "TestAllIndexers": "Afprøv alle indeks", "GrabReleases": "Hent udgivelse", "Link": "Links", "MappedDrivesRunningAsService": "Tilsluttede netværksdrev er ikke tilgængelige, når programmet kører som en Windows-tjeneste. Se FAQ'en for mere information", @@ -340,7 +340,7 @@ "Notification": "Notifikationer", "Remove": "Fjerne", "Replace": "erstat", - "TheLatestVersionIsAlreadyInstalled": "Den seneste version af {appName} er allerede installeret", + "OnLatestVersion": "Den seneste version af {appName} er allerede installeret", "Year": "År", "ApplyTagsHelpTextAdd": "Tilføj: Føj tags til den eksisterende liste over tags", "ApplyTagsHelpTextHowToApplyApplications": "Sådan anvendes tags på de valgte film", @@ -360,14 +360,73 @@ "DeleteAppProfileMessageText": "Er du sikker på, at du vil slette kvalitetsprofilen {0}", "RecentChanges": "Seneste ændringer", "WhatsNew": "Hvad er nyt?", - "ConnectionLostReconnect": "Radarr vil prøve at tilslutte automatisk, eller du kan klikke genindlæs forneden.", + "ConnectionLostReconnect": "{appName} vil prøve at tilslutte automatisk. Ellers du kan klikke genindlæs forneden.", "minutes": "Protokoller", "NotificationStatusAllClientHealthCheckMessage": "Alle lister er utilgængelige på grund af fejl", - "NotificationStatusSingleClientHealthCheckMessage": "Lister utilgængelige på grund af fejl: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Lister utilgængelige på grund af fejl: {notificationNames}", "AuthForm": "Formularer (login-side)", "DisabledForLocalAddresses": "Deaktiveret for lokale adresser", "ResetAPIKeyMessageText": "Er du sikker på, at du vil nulstille din API-nøgle?", "AuthBasic": "Grundlæggende (pop op-browser)", "None": "Ingen", - "RestartProwlarr": "Genstart {appName}" + "RestartProwlarr": "Genstart {appName}", + "AddCustomFilter": "Tilføj tilpasset filter", + "AddConnectionImplementation": "Tilføj forbindelse - {implementationName}", + "AddConnection": "Tilføj forbindelse", + "EditConnectionImplementation": "Tilføj forbindelse - {implementationName}", + "AddApplicationImplementation": "Tilføj forbindelse - {implementationName}", + "AddIndexerImplementation": "Tilføj indeksør - {implementationName}", + "ApplyChanges": "Anvend ændringer", + "AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}", + "Album": "album", + "Theme": "Tema", + "Categories": "Kategorier", + "Application": "Applikationer", + "EditIndexerProxyImplementation": "Tilføj betingelse - {implementationName}", + "Publisher": "Udgiver", + "IndexerHDBitsSettingsCodecs": "codec", + "Applications": "Applikationer", + "AddIndexerProxyImplementation": "Tilføj betingelse - {implementationName}", + "EditIndexerImplementation": "Tilføj betingelse - {implementationName}", + "Directory": "Mappe", + "EditApplicationImplementation": "Tilføj forbindelse - {implementationName}", + "EditDownloadClientImplementation": "Tilføj downloadklient - {implementationName}", + "IndexerHDBitsSettingsMediums": "Medium", + "CustomFilter": "Bruger Tilpassede Filtere", + "ProxyValidationBadRequest": "Kunne ikke teste proxy. Statuskode: {statusCode}", + "GrabRelease": "Hent udgivelse", + "Script": "Manuskript", + "BuiltIn": "Indbygget", + "PublishedDate": "Udgivelsesdato", + "AllSearchResultsHiddenByFilter": "Alle resultater skjules af det anvendte filter", + "AddApplication": "Tilføj Applikation", + "AddCategory": "Tilføj Kategori", + "ActiveApps": "Aktive Apps", + "NoEventsFound": "Ingen begivenheder fundet", + "AptUpdater": "Brug apt til at installere opdateringen", + "DockerUpdater": "opdater docker-containeren for at modtage opdateringen", + "Download": "Hent", + "ErrorRestoringBackup": "Fejl ved gendannelse af sikkerhedskopi", + "ExternalUpdater": "{appName} er konfigureret til at bruge en ekstern opdateringsmekanisme", + "RestartReloadNote": "Bemærk: {appName} genstarter automatisk og genindlæser brugergrænsefladen under gendannelsesprocessen.", + "UpdateAppDirectlyLoadError": "Kan ikke opdatere {appName} direkte,", + "InstallLatest": "Installer senest", + "Clone": "Luk", + "CurrentlyInstalled": "Aktuelt installeret", + "Stats": "Status", + "Mixed": "Fast", + "PrioritySettings": "Prioritet: {priority}", + "WouldYouLikeToRestoreBackup": "Vil du gendanne sikkerhedskopien »{name}«?", + "AppProfileSelectHelpText": "App-profiler bruges til at styre indstillinger for RSS, automatisk søgning og interaktiv søgning ved synkronisering med applikationer", + "ActiveIndexers": "Aktive indeksører", + "TheLogLevelDefault": "Logniveauet er som standard 'Info' og kan ændres under [Generelle indstillinger](/settings/general)", + "AddedToDownloadClient": "Udgivelse føjet til klient", + "AdvancedSettingsHiddenClickToShow": "Avancerede indstillinger er skjult. Klik for at vise", + "AdvancedSettingsShownClickToHide": "Avancerede indstillinger vises. Klik for at skjule", + "ApiKeyValidationHealthCheckMessage": "Opdater din API-nøgle til at være på mindste {length} karakterer. Dette kan gøres i indstillingerne eller i konfigurationsfilen", + "AppProfileInUse": "App-profil i brug", + "Episode": "afsnit", + "Artist": "kunstner", + "Id": "ID", + "Encoding": "Indkodning" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index d5be5ed3c..b07ede40b 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -17,11 +17,11 @@ "Age": "Alter", "All": "Alle", "AllIndexersHiddenDueToFilter": "Alle Indexer sind durch den ausgewählten Filter ausgeblendet.", - "Analytics": "Analytik", - "AnalyticsEnabledHelpText": "Sende anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen über Browser, welche Seiten der {appName}-Weboberfläche aufgerufen wurden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.", + "Analytics": "Analysen", + "AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.", "ApiKey": "API-Schlüssel", - "ApiKeyValidationHealthCheckMessage": "Bitte den API Schlüssel korrigieren, dieser muss mindestens {0} Zeichen lang sein. Die Änderung kann über die Einstellungen oder die Konfigurationsdatei erfolgen", - "AppDataDirectory": "AppData Ordner", + "ApiKeyValidationHealthCheckMessage": "Bitte den API Schlüssel korrigieren, dieser muss mindestens {length} Zeichen lang sein. Die Änderung kann über die Einstellungen oder die Konfigurationsdatei erfolgen", + "AppDataDirectory": "AppData-Verzeichnis", "AppDataLocationHealthCheckMessage": "Ein Update ist nicht möglich, um das Löschen von AppData beim Update zu verhindern", "AppProfileInUse": "App-Profil im Einsatz", "AppProfileSelectHelpText": "App-Profile werden verwendet, um die Einstellungen für RSS, automatische Suche und interaktive Suche bei der Anwendungssynchronisierung zu steuern", @@ -33,7 +33,7 @@ "ApplicationStatusCheckSingleClientMessage": "Applikationen wegen folgender Fehler nicht verfügbar: {0}", "Applications": "Anwendungen", "Apply": "Anwenden", - "ApplyTags": "Tags setzen", + "ApplyTags": "Schlagworte anwenden", "Apps": "Anwendungen", "AudioSearch": "Audio Suche", "Auth": "Authentifizierung", @@ -41,65 +41,65 @@ "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", - "Backup": "Backups", - "BackupFolderHelpText": "Relative Pfade befinden sich unter {appName}s AppData Ordner", - "BackupIntervalHelpText": "Intervall zum sichern der Datenbank und Einstellungen", + "Backup": "Sicherung", + "BackupFolderHelpText": "Relative Pfade befinden sich im AppData-Verzeichnis von {appName}", + "BackupIntervalHelpText": "Intervall zwischen automatischen Sicherungen", "BackupNow": "Jetzt sichern", - "BackupRetentionHelpText": "Automatische Backups, die älter als die Aufbewahrungsfrist sind, werden automatisch gelöscht", + "BackupRetentionHelpText": "Automatische Backups, die älter als der Aufbewahrungszeitraum sind, werden automatisch bereinigt", "Backups": "Sicherungen", "BeforeUpdate": "Vor dem Update", "BindAddress": "Adresse binden", - "BindAddressHelpText": "Gültige IP Adresse oder \"*\" für alle Netzwerke", + "BindAddressHelpText": "Gültige IP-Adresse, localhost oder „*“ für alle Schnittstellen", "BookSearch": "Buch Suche", "BookSearchTypes": "Buch-Suchtypen", - "Branch": "Git-Branch", - "BranchUpdate": "Verwendeter Branch zur Aktualisierung von {appName}", + "Branch": "Branch", + "BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten", "BranchUpdateMechanism": "Git-Branch für den externen Updateablauf", "BypassProxyForLocalAddresses": "Proxy für lokale Adressen umgehen", "Cancel": "Abbrechen", - "CancelPendingTask": "Diese laufende Aufgabe wirklich abbrechen?", + "CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?", "Categories": "Kategorien", "Category": "Kategorie", - "CertificateValidation": "Zertifikat Validierung", + "CertificateValidation": "Zertifikatsvalidierung", "CertificateValidationHelpText": "Ändere wie streng die Validierung der HTTPS-Zertifizierung ist", "ChangeHasNotBeenSavedYet": "Änderung wurde noch nicht gespeichert", "Clear": "Leeren", "ClearHistory": "Verlauf leeren", "ClearHistoryMessageText": "Wirklich den ganzen {appName} Verlauf löschen?", "ClientPriority": "Priorität", - "CloneProfile": "Profil kopieren", + "CloneProfile": "Profil klonen", "Close": "Schließen", "CloseCurrentModal": "Momentanes Modal schließen", "Columns": "Spalten", "Component": "Komponente", "Connect": "Benachrichtigungen", - "ConnectSettings": "Eintellungen für Verbindungen", + "ConnectSettings": "Verbindungseinstellungen", "ConnectSettingsSummary": "Benachrichtigungen und eigene Scripte", "ConnectionLost": "Verbindung unterbrochen", "Connections": "Verbindungen", "CouldNotConnectSignalR": "Es konnte keine Verbindung zu SignalR hergestellt werden, die Benutzeroberfläche wird nicht aktualisiert", "Custom": "Benutzerdefiniert", - "CustomFilters": "Eigene Filter", - "DBMigration": "DB Migration", + "CustomFilters": "Benutzerdefinierte Filter", + "DatabaseMigration": "DB Migration", "Database": "Datenbank", "Date": "Datum", "Dates": "Termine", "Delete": "Löschen", "DeleteAppProfile": "App-Profil löschen", "DeleteApplication": "Applikation löschen", - "DeleteApplicationMessageText": "Wirklich die Applikation '{0}' löschen?", - "DeleteBackup": "Backup löschen", - "DeleteBackupMessageText": "Backup '{0}' wirkich löschen?", - "DeleteDownloadClient": "Downloader löschen", - "DeleteDownloadClientMessageText": "Downloader '{0}' wirklich löschen?", + "DeleteApplicationMessageText": "Bist du sicher, dass du die Anwendung „{name}“ löschen möchtest?", + "DeleteBackup": "Sicherung löschen", + "DeleteBackupMessageText": "Soll das Backup '{name}' wirklich gelöscht werden?", + "DeleteDownloadClient": "Download-Client löschen", + "DeleteDownloadClientMessageText": "Bist du sicher, dass du den Download Client '{name}' wirklich löschen willst?", "DeleteIndexerProxy": "Indexer Proxy löschen", - "DeleteIndexerProxyMessageText": "Tag '{0}' wirklich löschen?", + "DeleteIndexerProxyMessageText": "Bist du sicher, dass du den Indexer-Proxy „{name}“ löschen möchtest?", "DeleteNotification": "Benachrichtigung löschen", - "DeleteNotificationMessageText": "Benachrichtigung '{0}' wirklich löschen?", + "DeleteNotificationMessageText": "Bist du sicher, dass du die Benachrichtigung '{name}' wirklich löschen willst?", "DeleteTag": "Tag löschen", - "DeleteTagMessageText": "Tag '{0}' wirklich löschen?", + "DeleteTagMessageText": "Bist du sicher, dass du den Tag '{label}' wirklich löschen willst?", "Description": "Beschreibung", - "Details": "Details", + "Details": "Einzelheiten", "DevelopmentSettings": "Entwicklungseinstellungen", "Disabled": "Deaktiviert", "Discord": "Discord", @@ -107,9 +107,9 @@ "Donations": "Spenden", "DownloadClient": "Downloader", "DownloadClientSettings": "Downloader Einstellungen", - "DownloadClientStatusCheckAllClientMessage": "Alle Download Clients sind aufgrund von Fehlern nicht verfügbar", - "DownloadClientStatusCheckSingleClientMessage": "Download Clients aufgrund von Fehlern nicht verfügbar: {0}", - "DownloadClients": "Downloader", + "DownloadClientStatusAllClientHealthCheckMessage": "Alle Download Clients sind aufgrund von Fehlern nicht verfügbar", + "DownloadClientStatusSingleClientHealthCheckMessage": "Download Clients aufgrund von Fehlern nicht verfügbar: {downloadClientNames}", + "DownloadClients": "Download Clients", "DownloadClientsSettingsSummary": "Download der Client-Konfigurationen für die Integration in die {appName} UI-Suche", "Duration": "Dauer", "Edit": "Bearbeiten", @@ -118,10 +118,10 @@ "ElapsedTime": "Vergangene Zeit", "Enable": "Aktivieren", "EnableAutomaticSearch": "Automatische Suche einschalten", - "EnableAutomaticSearchHelpText": "Wird für automatische Suchen genutzt die vom Benutzer oder von {appName} gestartet werden", + "EnableAutomaticSearchHelpText": "Wird verwendet, wenn die automatische Suche über die Benutzeroberfläche oder durch {appName} durchgeführt wird.", "EnableIndexer": "Indexer aktivieren", "EnableInteractiveSearch": "Interaktive Suche einschalten", - "EnableInteractiveSearchHelpText": "Wird bei der manuellen Suche benutzt", + "EnableInteractiveSearchHelpText": "Wird verwendet, wenn die interaktive Suche verwendet wird", "EnableRss": "RSS aktivieren", "EnableRssHelpText": "RSS-Feed für Indexer aktivieren", "EnableSSL": "SSL", @@ -131,13 +131,13 @@ "Encoding": "Codierung", "Ended": "Beendet", "Error": "Fehler", - "ErrorLoadingContents": "Fehler beim laden der Inhalte", - "EventType": "Event Typ", - "Events": "Events", + "ErrorLoadingContents": "Fehler beim Laden von Inhalten", + "EventType": "Ereignistyp", + "Events": "Ereignisse", "Exception": "Ausnahme", "ExistingTag": "Vorhandener Tag", "Failed": "Fehlgeschlagen", - "FeatureRequests": "Funktionsanfragen", + "FeatureRequests": "Feature Anfragen", "Filename": "Dateiname", "Files": "Dateien", "Filter": "Filter", @@ -153,19 +153,19 @@ "GeneralSettingsSummary": "Port, SSL, Benutzername/Passwort, Proxy, Analytik und Updates", "GrabReleases": "Release erfassen", "GrabTitle": "Titel holen", - "Grabbed": "Erfasste", + "Grabbed": "Geholt", "Grabs": "Erfasse", - "Health": "Zustandsüberwachung", - "HealthNoIssues": "Keine Probleme mit deiner Konfiguration", - "HideAdvanced": "Erweiterte Ansicht", + "Health": "Gesundheit", + "NoIssuesWithYourConfiguration": "Keine Probleme mit deiner Konfiguration", + "HideAdvanced": "Erweiterte Einstellungen ausblenden", "History": "Verlauf", "HistoryCleanup": "Verlaufsbereinigung", "HistoryCleanupDaysHelpText": "Auf 0 setzen um das automatische leeren des Papierkorbs zu deaktivieren", "HistoryCleanupDaysHelpTextWarning": "Datien im Papierkorb die älter sind als der gewählte Wert, werden endgültig gelöscht", - "HomePage": "Startseite", + "HomePage": "Hauptseite", "Host": "Host", "Hostname": "Hostname", - "Id": "Id", + "Id": "ID", "IgnoredAddresses": "Ignorierte Adressen", "IllRestartLater": "Später neustarten", "IncludeHealthWarningsHelpText": "Zustandswarnung", @@ -176,51 +176,51 @@ "IndexerFlags": "Indexer-Flags", "IndexerHealthCheckNoIndexers": "Keine Indexer aktiviert, {appName} wird keine Suchergebnisse zurückgeben", "IndexerInfo": "Indexer-Info", - "IndexerLongTermStatusCheckAllClientMessage": "Alle Indexer sind wegen über 6 Stunden langen bestehender Fehler nicht verfügbar", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexer wegen über 6 Stunden langen bestehenden Fehlern nicht verfügbar: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind aufgrund von Fehlern länger als 6 Stunden nicht verfügbar", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexer sind aufgrund von Fehlern länger als 6 Stunden nicht verfügbar: {indexerNames}", "IndexerName": "Indexer-Name", - "IndexerNoDefCheckMessage": "Indexer haben keine Definition und werden nicht funktionieren: {0}. Bitte entferne und (oder) füge diese neu zu {appName} hinzu", + "IndexerNoDefinitionCheckHealthCheckMessage": "Indexer haben keine Definition und werden nicht funktionieren: {indexerNames}. Bitte entferne und (oder) füge diese neu zu {appName} hinzu", "IndexerObsoleteCheckMessage": "Indexer sind nicht mehr verfügbar oder wurden aktualiiert: {0}. Bitte enfernen und (oder) neu zu {appName} hinzufügen", - "IndexerPriority": "Priorität", + "IndexerPriority": "Indexer-Priorität", "IndexerPriorityHelpText": "Indexer Priorität von 1 (höchste) bis 50 (niedrigste). Standard: 25.", "IndexerProxies": "Indexer-Proxies", "IndexerProxy": "Indexer-Proxy", - "IndexerProxyStatusCheckAllClientMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar", - "IndexerProxyStatusCheckSingleClientMessage": "Listen aufgrund von Fehlern nicht verfügbar: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Listen aufgrund von Fehlern nicht verfügbar: {indexerProxyNames}", "IndexerQuery": "Indexer Anfrage", "IndexerRss": "Indexer RSS", "IndexerSettingsSummary": "Konfiguration verschiedener globaler Indexer Einstellungen, einschließlich Proxies.", "IndexerSite": "Indexer-Seite", - "IndexerStatusCheckAllClientMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar", - "IndexerStatusCheckSingleClientMessage": "Indexer aufgrund von Fehlern nicht verfügbar: {0}", - "IndexerTagsHelpText": "Benutze Tags, um Indexer-Proxies zu spezifizieren, mit welchen Apps der Indexer synchronisiert oder um Indexer zu organisieren.", - "IndexerVipCheckExpiredClientMessage": "Die VIP Indexer Vorteile sind abgelaufen: {0}", - "IndexerVipCheckExpiringClientMessage": "Die Indexer VIP Vorteile verfallen bald: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Alle Indexer sind aufgrund von Fehlern nicht verfügbar", + "IndexerStatusUnavailableHealthCheckMessage": "Indexer nicht verfügbar aufgrund von Fehlern: {indexerNames}", + "IndexerTagsHelpText": "Verwende Tags, um Indexer-Proxys oder die Apps, mit denen der Indexer synchronisiert wird, anzugeben.", + "IndexerVipExpiredHealthCheckMessage": "Die VIP Indexer Vorteile sind abgelaufen: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Die Indexer VIP Vorteile verfallen bald: {indexerNames}", "Indexers": "Indexer", "Info": "Info", "InstanceName": "Instanzname", - "InstanceNameHelpText": "Instanzname im Browser-Tab und für Syslog-Anwendungsname", + "InstanceNameHelpText": "Instanzname im Tab und für den Syslog-App-Namen", "InteractiveSearch": "Interaktive Suche", "Interval": "Intervall", "KeyboardShortcuts": "Tastenkürzel", "Language": "Sprache", "LastDuration": "Letzte Dauer", "LastExecution": "Letzte Ausführung", - "LastWriteTime": "Zuletzt beschrieben", + "LastWriteTime": "Letzte Schreibzeit", "LaunchBrowserHelpText": " Öffne die Startseite von {appName} im Webbrowser nach dem Start.", - "Level": "Stufe", + "Level": "Level", "Link": "Links", - "LogFiles": "Protokolle", - "LogLevel": "Log Level", - "LogLevelTraceHelpTextWarning": "Trace logging sollte nur kurzzeitig aktiviert werden", + "LogFiles": "Protokolldateien", + "LogLevel": "Protokollstufe", + "LogLevelTraceHelpTextWarning": "Die Trace-Protokollierung sollte nur vorübergehend aktiviert werden", "Logging": "Protokollierung", "Logs": "Protokolle", "MIA": "MIA", - "MaintenanceRelease": "Wartung: Fehlerbehebung und andere Verbesserungen. Siehe Github Commit History für weitere Details", + "MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details", "Manual": "Manuell", "MappedDrivesRunningAsService": "Zugeordnete Netzlaufwerke sind nicht verfügbar, wenn {appName} als Windows-Dienst ausgeführt wird. Bitte lesen Sie die FAQ für weitere Informationen", "MassEditor": "Masseneditor", - "Mechanism": "Verfahren", + "Mechanism": "Mechanismus", "Message": "Nachricht", "MinimumSeeders": "Mindest-Seeder", "MinimumSeedersHelpText": "Minimale Anzahl an Seedern die von der Anwendung benötigt werden um den Indexer zu holen", @@ -236,67 +236,67 @@ "New": "Neu", "NextExecution": "Nächste Ausführung", "No": "Nein", - "NoBackupsAreAvailable": "Es sind keine Backups vorhanden", + "NoBackupsAreAvailable": "Keine Sicherungen verfügbar", "NoChange": "Keine Änderung", "NoChanges": "Keine Änderungen", - "NoLeaveIt": "Nein, nicht ändern", + "NoLeaveIt": "Nein, lass es", "NoLinks": "Keine Links", - "NoLogFiles": "Keine Log-Dateien", + "NoLogFiles": "Keine Logdateien", "NoSearchResultsFound": "Keine Suchergebnisse gefunden. Versuchen Sie unten eine erneute Suche durchzuführen.", - "NoTagsHaveBeenAddedYet": "Es wurden noch keine Tags erstellt", + "NoTagsHaveBeenAddedYet": "Es wurden noch keine Tags hinzugefügt", "NoUpdatesAreAvailable": "Es sind keine Updates verfügbar", "NotSupported": "Nicht unterstützt", "Notification": "Benachrichtigungen", - "NotificationTriggers": "Benachrichtigungs Auslöser", - "NotificationTriggersHelpText": "Auslöser für diese Benachrichtigung auswählen", + "NotificationTriggers": "Benachrichtigungs-Auslöser", + "NotificationTriggersHelpText": "Wähle aus, welche Ereignisse diese Benachrichtigung auslösen sollen", "Notifications": "Benachrichtigungen", "OAuthPopupMessage": "Dein Browser blockiert Pop-ups", - "Ok": "OK", + "Ok": "Ok", "OnApplicationUpdate": "Bei Anwendungsaktualisierung", "OnApplicationUpdateHelpText": "Bei Anwendungsaktualisierung", - "OnGrab": "Bei Erfassung", - "OnHealthIssue": "Bei Zustandsproblem", + "OnGrab": "Bei Release-Grabs", + "OnHealthIssue": "Bei Gesundheitsproblem", "OnHealthIssueHelpText": "Zustandsproblem", "OpenBrowserOnStart": "Browser beim Start öffnen", "OpenThisModal": "Dieses Modal öffnen", "Options": "Optionen", - "PackageVersion": "Paket Version", + "PackageVersion": "Paketversion", "PageSize": "Einträge pro Seite", "PageSizeHelpText": "Anzahl der Einträge pro Seite", "Parameters": "Parameter", "Password": "Passwort", "Peers": "Peers", - "PendingChangesDiscardChanges": "Änderungen verwerfen und schließen", - "PendingChangesMessage": "Es gibt noch ungespeicherte Änderungen, bist du sicher, dass du die Seite verlassen möchtest?", - "PendingChangesStayReview": "Auf der Seite bleiben", + "PendingChangesDiscardChanges": "Änderungen verwerfen und verlassen", + "PendingChangesMessage": "Du hast ungespeicherte Änderungen, bist du sicher, dass du diese Seite verlassen möchtest?", + "PendingChangesStayReview": "Bleiben und Änderungen überprüfen", "Port": "Port", - "PortNumber": "Port Nummer", + "PortNumber": "Portnummer", "Presets": "Voreinstellungen", "Priority": "Priorität", "Privacy": "Privatsphäre", "Private": "Privat", "Protocol": "Protokoll", - "ProwlarrSupportsAnyDownloadClient": "Jeder Downloader der den Newznab-Standard verwendet oder unten aufgelistet ist wird untertützt.", + "ProwlarrSupportsAnyDownloadClient": "{appName} unterstützt jeden der unten aufgeführten Download-Clients.", "ProwlarrSupportsAnyIndexer": "{appName} unterstützt alle Indexer, welcher den Newznab/Torznab Standard implementiert (verwende 'Generic Newznab' (für Usenet) oder 'Generic Torznab' (für Torrents)) und darüber hinaus viele weitere Indexer. Wählen Sie im Folgenden Ihren Indexer aus der Liste.", "Proxies": "Proxies", "Proxy": "Proxy", - "ProxyBypassFilterHelpText": "Verwende ',' als Trennzeichen und '*.' als Platzhalter für Subdomains", - "ProxyCheckBadRequestMessage": "Proxy konnte nicht getestet werden. StatusCode: {0}", - "ProxyCheckFailedToTestMessage": "Proxy konnte nicht getestet werden: {0}", - "ProxyCheckResolveIpMessage": "Fehler beim Auflösen der IP-Adresse für den konfigurierten Proxy-Host {0}", - "ProxyPasswordHelpText": "Nur wenn ein Benutzername und Passwort erforderlich ist, muss es eingegeben werden. Ansonsten leer lassen.", - "ProxyType": "Proxy Typ", - "ProxyUsernameHelpText": "Nur wenn ein Benutzername und Passwort erforderlich ist, muss es eingegeben werden. Ansonsten leer lassen.", + "ProxyBypassFilterHelpText": "Verwenden Sie ',' als Trennzeichen und '*.' als Wildcard für Subdomains", + "ProxyBadRequestHealthCheckMessage": "Proxy konnte nicht getestet werden. StatusCode: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Proxy konnte nicht getestet werden: {url}", + "ProxyResolveIpHealthCheckMessage": "Fehler beim Auflösen der IP-Adresse für den konfigurierten Proxy-Host {proxyHostName}", + "ProxyPasswordHelpText": "Sie müssen nur einen Benutzernamen und ein Passwort eingeben, wenn dies erforderlich ist. Andernfalls lassen Sie sie leer.", + "ProxyType": "Proxy-Typ", + "ProxyUsernameHelpText": "Sie müssen nur einen Benutzernamen und ein Passwort eingeben, wenn dies erforderlich ist. Andernfalls lassen Sie sie leer.", "Public": "Öffentlich", "Query": "Abfrage", "QueryOptions": "Abfrage-Optionen", "QueryResults": "Abfrageergebnisse", "Queue": "Warteschlange", - "Queued": "In der Warteschlange", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS wird von diesem Indexer nicht unterstützt", + "Queued": "In Warteschlange", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS wird mit diesem Indexer nicht unterstützt", "RawSearchSupported": "Raw-Suche unterstützt", - "ReadTheWikiForMoreInformation": "Lese das Wiki für mehr Informationen", + "ReadTheWikiForMoreInformation": "Lesen Sie das Wiki für weitere Informationen", "Reddit": "Reddit", "Redirect": "Umleiten", "RedirectHelpText": "Eingehende Download-Anfragen für den Indexer umleiten, anstatt Proxying mit {appName}", @@ -304,22 +304,22 @@ "RefreshMovie": "Film aktualisieren", "ReleaseBranchCheckOfficialBranchMessage": "Zweig {0} ist kein gültiger {appName}-Release-Zweig. Sie erhalten keine Updates", "ReleaseStatus": "Releasestatus", - "Reload": "Neuladen", + "Reload": "Neu laden", "Remove": "Entfernen", "RemoveFilter": "Filter entfernen", - "RemovedFromTaskQueue": "Aus der Aufgabenwarteschlage entfernt", + "RemovedFromTaskQueue": "Aus der Aufgabenwarteschlange entfernt", "RemovingTag": "Tag entfernen", "Replace": "Ersetzen", "Reset": "Zurücksetzen", "ResetAPIKey": "API-Schlüssel zurücksetzen", - "Restart": "Neustarten", + "Restart": "Neu starten", "RestartNow": "Jetzt neustarten", "RestartProwlarr": "{appName} Neustarten", - "RestartRequiredHelpTextWarning": "Erfordert einen Neustart", + "RestartRequiredHelpTextWarning": "Erfordert einen Neustart, damit die Aktion wirksam wird", "Restore": "Wiederherstellen", - "RestoreBackup": "Backup einspielen", + "RestoreBackup": "Sicherung wiederherstellen", "Result": "Ergebnis", - "Retention": "Aufbewahrung ( Retention )", + "Retention": "Aufbewahrung", "SSLCertPassword": "SSL Zertifikat Passwort", "SSLCertPasswordHelpText": "Passwort für die PFX Datei", "SSLCertPath": "Pfad zum SSL Zertifikat", @@ -329,8 +329,8 @@ "SaveChanges": "Änderungen speichern", "SaveSettings": "Einstellungen speichern", "Scheduled": "Geplant", - "ScriptPath": "Script Pfad", - "Search": "Suche", + "ScriptPath": "Skript-Pfad", + "Search": "Suchen", "SearchCapabilities": "Suchfähigkeiten", "SearchIndexers": "Indexer suchen", "SearchType": "Suchtyp", @@ -339,8 +339,8 @@ "Seeders": "Seeders", "SelectAll": "Alle wählen", "SemiPrivate": "Halbprivat", - "SendAnonymousUsageData": "Anonyme Nutzungsdaten übertragen", - "SetTags": "Tags setzen", + "SendAnonymousUsageData": "Sende anonyme Nutzungsdaten", + "SetTags": "Tags festlegen", "Settings": "Einstellungen", "SettingsConsoleLogLevel": "Konsolen Log Level", "SettingsEnableColorImpairedMode": "Farbbeeinträchtigter Modus aktivieren", @@ -358,15 +358,15 @@ "SettingsShowRelativeDatesHelpText": "Relatives (z.B.: Heute, gestern, etc) oder absolutes Datum anzeigen", "SettingsSqlLoggingHelpText": "Log alle SQL Abfragen von {appName}", "SettingsTimeFormat": "Zeitformat", - "ShowAdvanced": "Einfache Ansicht", + "ShowAdvanced": "Erweitert anzeigen", "ShowSearch": "Suche anzeigen", - "ShowSearchHelpText": "Suchbutton anzeigen beim draufzeigen", + "ShowSearchHelpText": "Suchschaltfläche beim Überfahren anzeigen", "Shutdown": "Herunterfahren", "Size": "Größe", "Sort": "Sortieren", "Source": "Quelle", "StartTypingOrSelectAPathBelow": "Eingeben oder unten auswählen", - "Started": "gestartet", + "Started": "Gestartet", "StartupDirectory": "Start-Verzeichnis", "Stats": "Statistiken", "Status": "Status", @@ -379,23 +379,23 @@ "SyncProfile": "Sync-Profile", "SyncProfiles": "Sync-Profile", "System": "System", - "SystemTimeCheckMessage": "Die Systemzeit ist um einen Tag versetzt. Bis die Zeit korrigiert wurde, könnten die geplanten Aufgaben nicht korrekt ausgeführt werden", + "SystemTimeHealthCheckMessage": "Die Systemzeit ist um einen Tag versetzt. Bis die Zeit korrigiert wurde, könnten die geplanten Aufgaben nicht korrekt ausgeführt werden", "TVSearchTypes": "Suchtyp", - "TableOptions": "Tabellen Optionen", + "TableOptions": "Tabellenoptionen", "TableOptionsColumnsMessage": "Wähle aus welche Spalten angezeigt werden und in welcher Reihenfolge", "TagCannotBeDeletedWhileInUse": "Kann während der Benutzung nicht gelöscht werden", - "TagIsNotUsedAndCanBeDeleted": "Tag wird nicht benutzt und kann gelöscht werden", + "TagIsNotUsedAndCanBeDeleted": "Tag wird nicht verwendet und kann gelöscht werden", "Tags": "Tags", "TagsHelpText": "Wird auf Filme mit mindestens einem passenden Tag angewandt", - "TagsSettingsSummary": "Alle Tags und deren Benutzung anzeigen. Unbenutzte Tags können entfernt werden", + "TagsSettingsSummary": "Sehen Sie sich alle Tags und deren Verwendung an. Nicht verwendete Tags können entfernt werden", "Tasks": "Aufgaben", - "Test": "Testen", - "TestAll": "Alle testen", + "Test": "Prüfen", + "TestAll": "Alle prüfen", "TestAllApps": "Alle Apps testen", - "TestAllClients": "Alle testen", - "TestAllIndexers": "Alle testen", - "TheLatestVersionIsAlreadyInstalled": "Die aktuellste Version ist bereits installiert", - "ThemeHelpText": "Ändere das UI-Theme der Anwendung. Das 'Auto'-Theme verwendet dein Betriebssystem-Theme, um den hellen oder dunklen Modus einzustellen. Inspiriert von {0}", + "TestAllClients": "Prüfe alle Clients", + "TestAllIndexers": "Prüfe alle Indexer", + "OnLatestVersion": "Die neueste Version von {appName} ist bereits installiert", + "ThemeHelpText": "Ändere das UI-Design der Anwendung, das 'Auto'-Design verwendet das Betriebssystem-Design, um den Hell- oder Dunkelmodus festzulegen. Inspiriert von {inspiredBy}.", "Time": "Zeit", "Title": "Titel", "Today": "Heute", @@ -406,7 +406,7 @@ "Type": "Typ", "UI": "Oberfläche", "UILanguage": "Oberflächen Sprache ( UI Language )", - "UILanguageHelpText": "Sprache für die gesamte Oberfläche", + "UILanguageHelpText": "Sprache, die {appName} für die Benutzeroberfläche verwenden wird", "UILanguageHelpTextWarning": "Webseite muss neu geladen werden", "UISettings": "Benutzeroberflächen Einstellungen", "UISettingsSummary": "Optionen für Datum, Sprache und Farbbeinträchtigungen", @@ -418,10 +418,10 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "Der neue Indexer konnte nicht hinzugefügt werden, bitte erneut probieren.", "UnableToAddANewNotificationPleaseTryAgain": "Die neue Benachrichtigung konnte nicht hinzugefügt werden, bitte erneut probieren.", "UnableToLoadAppProfiles": "App-Profile können nicht geladen werden", - "UnableToLoadApplicationList": "Anwendungsliste kann nicht geladen werden", - "UnableToLoadBackups": "Backups konnten nicht geladen werden", + "ApplicationsLoadError": "Anwendungsliste kann nicht geladen werden", + "BackupsLoadError": "Sicherungen können nicht geladen werden", "UnableToLoadDevelopmentSettings": "Entwicklereinstellungen konnten nicht geladen werden", - "UnableToLoadDownloadClients": "Downloader konnten nicht geladen werden", + "DownloadClientsLoadError": "Download Clients können nicht geladen werden", "UnableToLoadGeneralSettings": "Allgemeine Einstellungen konnten nicht geladen werden", "UnableToLoadHistory": "Verlauf konnte nicht geladen werden", "UnableToLoadIndexerProxies": "Indexer-Proxies können nicht geladen werden", @@ -429,41 +429,41 @@ "UnableToLoadNotifications": "Benachrichtigungen konnten nicht geladen werden", "UnableToLoadTags": "Tags konnten nicht geladen werden", "UnableToLoadUISettings": "Oberflächen Einstellungen konnten nicht geladen werden", - "UnsavedChanges": "Ungespeicherte Änderungen", - "UnselectAll": "Keine wählen", - "UpdateAutomaticallyHelpText": "Updates automatisch herunteraden und installieren. Es kann weiterhin unter \"System -> Updates\" ein manuelles Update angestoßen werden", - "UpdateCheckStartupNotWritableMessage": "Update kann nicht installiert werden, da der Startordner '{0}' vom Benutzer '{1}' nicht beschreibbar ist.", - "UpdateCheckStartupTranslocationMessage": "Update kann nicht installiert werden, da sich der Startordner '{0}' in einem App Translocation-Ordner befindet.", - "UpdateCheckUINotWritableMessage": "Update kann nicht installiert werden, da der Benutzeroberflächenordner '{0}' vom Benutzer '{1}' nicht beschreibbar ist.", - "UpdateMechanismHelpText": "Benutze {appName}'s Built-In Updater oder ein Script", + "UnsavedChanges": "Nicht gespeicherte Änderungen", + "UnselectAll": "Alle abwählen", + "UpdateAutomaticallyHelpText": "Updates automatisch herunterladen und installieren. Sie können weiterhin über System: Updates installieren", + "UpdateStartupNotWritableHealthCheckMessage": "Update kann nicht installiert werden, da der Startordner '{startupFolder}' vom Benutzer '{userName}' nicht beschreibbar ist.", + "UpdateStartupTranslocationHealthCheckMessage": "Update kann nicht installiert werden, da sich der Startordner '{startupFolder}' in einem App Translocation-Ordner befindet.", + "UpdateUiNotWritableHealthCheckMessage": "Update kann nicht installiert werden, da der Benutzeroberflächenordner '{uiFolder}' vom Benutzer '{userName}' nicht beschreibbar ist.", + "UpdateMechanismHelpText": "Verwenden Sie den integrierten Updater von {appName} oder ein Skript", "UpdateScriptPathHelpText": "Pfad zu einem benutzerdefinierten Skript, das ein extrahiertes Update-Paket übernimmt und den Rest des Update-Prozesses abwickelt", - "Updates": "Updates", - "Uptime": "Laufzeit", + "Updates": "Aktualisierung", + "Uptime": "Betriebszeit", "Url": "Url", - "UrlBaseHelpText": "Für Reverse-Proxy-Unterstützung. Die Standardeinstellung leer", - "UseProxy": "Proxy benutzen", + "UrlBaseHelpText": "Für die Reverse-Proxy-Unterstützung ist der Standardwert leer", + "UseProxy": "Verwende Proxy", "Usenet": "Usenet", "UserAgentProvidedByTheAppThatCalledTheAPI": "UserAgent von der App welcher die API aufgerufen hat", - "Username": "Benutzername", + "Username": "Nutzername", "Version": "Version", "View": "Ansicht", - "Warn": "Warnung", + "Warn": "Achtung", "Website": "Webseite", "Wiki": "Wiki", "Yes": "Ja", - "YesCancel": "Ja, abbrechen", + "YesCancel": "Ja Abbrechen", "Yesterday": "Gestern", "OnHealthRestoredHelpText": "Bei Wiederherstellung des Zustands", - "OnHealthRestored": "Bei Wiederherstellung des Zustands", + "OnHealthRestored": "Bei Wiederherstellung der Gesundheit", "StopSelecting": "Auswahl stoppen", "ApplicationURL": "Anwendungs-URL", - "ApplicationUrlHelpText": "Die externe URL der Anwendung inklusive http(s)://, Port und URL-Basis", + "ApplicationUrlHelpText": "Die externe URL dieser Anwendung, einschließlich http(s)://, Port und URL-Basis", "ApplyChanges": "Änderungen anwenden", - "CountIndexersSelected": "{0} Indexer ausgewählt", + "CountIndexersSelected": "{count} Indexer ausgewählt", "DeleteSelectedDownloadClients": "Lösche Download Client(s)", - "DeleteSelectedApplicationsMessageText": "Indexer '{0}' wirklich löschen?", - "DeleteSelectedDownloadClientsMessageText": "Indexer '{0}' wirklich löschen?", - "DeleteSelectedIndexersMessageText": "Indexer '{0}' wirklich 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-Clients löschen möchten?", + "DeleteSelectedIndexersMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte(n) Indexer löschen möchten?", "EditSelectedDownloadClients": "Ausgewählte Download Clienten bearbeiten", "Implementation": "Integration", "ManageDownloadClients": "Verwalte Download Clienten", @@ -471,11 +471,11 @@ "NoIndexersFound": "Keine Indexer gefunden", "Theme": "Design", "Season": "Staffel", - "ApplyTagsHelpTextAdd": "Hinzufügen: Füge neu Tags zu den existierenden Tags hinzu", + "ApplyTagsHelpTextAdd": "Hinzufügen: Füge Tags zu den bestehenden Tags hinzu", "ApplyTagsHelpTextHowToApplyApplications": "Wie werden Tags zu ausgewählten Autoren zugeteilt", - "ApplyTagsHelpTextHowToApplyIndexers": "Wie werden Tags zu ausgewählten Indexern zugeteilt", - "ApplyTagsHelpTextRemove": "Entfernen: Eingegebene Tags entfernen", - "ApplyTagsHelpTextReplace": "Ersetzen: Nur eingegebene Tags übernehmen und vorhandene entfernen( keine Tags eingeben um alle zu entfernen )", + "ApplyTagsHelpTextHowToApplyIndexers": "Wie Tags zu den selektierten Indexern hinzugefügt werden können", + "ApplyTagsHelpTextRemove": "Entfernen: Entferne die hinterlegten Tags", + "ApplyTagsHelpTextReplace": "Ersetzen: Ersetze die Tags mit den eingegebenen Tags (keine Tags eingeben um alle Tags zu löschen)", "DownloadClientPriorityHelpText": "Priorisiere mehrere Downloader. Rundlauf-Verfahren wird für Downloader mit der gleichen Priorität verwendet.", "EditSelectedIndexers": "Ausgewähle Indexer bearbeiten", "SelectIndexers": "Indexer suchen", @@ -485,26 +485,26 @@ "More": "Mehr", "Publisher": "Herausgeber", "Track": "Trace", - "UpdateAvailable": "Neue Version verfügbar", + "UpdateAvailableHealthCheckMessage": "Ein neues Update ist verfügbar: {version}", "Year": "Jahr", "Album": "Album", - "Artist": "Künstler", + "Artist": "Interpret", "Author": "Autor", "Book": "Buch", - "ConnectionLostReconnect": "Radarr wird automatisch versuchen zu verbinden oder klicke unten auf neuladen.", - "ConnectionLostToBackend": "Radarr hat die Verbindung zum Backend verloren und muss neugeladen werden.", - "RecentChanges": "Neuste Änderungen", - "WhatsNew": "Was gibt's Neues?", + "ConnectionLostReconnect": "{appName} wird versuchen, automatisch 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?", "minutes": "Minuten", "DeleteAppProfileMessageText": "Qualitätsprofil '{0}' wirklich löschen?", "AddConnection": "Verbindung hinzufügen", - "NotificationStatusAllClientHealthCheckMessage": "Wegen Fehlern sind keine Applikationen verfügbar", - "NotificationStatusSingleClientHealthCheckMessage": "Applikationen wegen folgender Fehler nicht verfügbar: {0}", - "AuthBasic": "Einfach (Browser Popup)", - "AuthForm": "Formular (Login Seite)", - "DisabledForLocalAddresses": "Für Lokale Adressen deaktivieren", + "NotificationStatusAllClientHealthCheckMessage": "Alle Benachrichtigungen sind aufgrund von Fehlern nicht verfügbar", + "NotificationStatusSingleClientHealthCheckMessage": "Benachrichtigungen nicht verfügbar wegen Fehlern: {notificationNames}", + "AuthBasic": "Basis (Browser-Popup)", + "AuthForm": "Formulare (Anmeldeseite)", + "DisabledForLocalAddresses": "Für lokale Adressen deaktiviert", "None": "Keine", - "ResetAPIKeyMessageText": "Bist du sicher, dass du den API-Schlüssel zurücksetzen willst?", + "ResetAPIKeyMessageText": "Sind Sie sicher, dass Sie Ihren API-Schlüssel zurücksetzen möchten?", "AddCustomFilter": "Eigenen Filter hinzufügen", "AddApplication": "Application hinzufügen", "AddCategory": "Kategorie hinzufügen", @@ -516,5 +516,295 @@ "VipExpiration": "VIP Ablaufdatum", "TotalUserAgentQueries": "Gesamte Nutzeragent Anfragen", "ActiveApps": "Aktive Apps", - "ActiveIndexers": "Aktive Indexer" + "ActiveIndexers": "Aktive Indexer", + "AppsMinimumSeeders": "Apps Mindestanzahl von Seedern", + "ApplicationTagsHelpText": "Indexer mit dieser Anwendung synchronisieren, die mindestens einen übereinstimmenden Tag haben. Wenn hier keine Tags aufgeführt sind, wird kein Indexer aufgrund seiner Tags von der Synchronisierung ausgeschlossen.", + "ApplicationTagsHelpTextWarning": "Tags sollten mit Vorsicht verwendet werden, da sie ungewollte Effekte haben können. Eine Anwendung mit einem Tag synchronisiert nur Indexer die den Gleichen Tag haben.", + "AddApplicationImplementation": "Anwendung hinzufügen - {implementationName}", + "AddConnectionImplementation": "Verbindung hinzufügen - {implementationName}", + "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 vermeiden benötigt {appName} jetzt , dass Authentifizierung eingeschaltet ist. Du kannst Authentifizierung optional für lokale Adressen ausschalten.", + "AuthenticationRequired": "Authentifizierung benötigt", + "AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.", + "AuthenticationRequiredUsernameHelpTextWarning": "Neuen Benutzernamen eingeben", + "AuthenticationMethodHelpTextWarning": "Bitte wähle eine gültige Authentifizierungsmethode aus", + "AuthenticationRequiredPasswordHelpTextWarning": "Neues Passwort eingeben", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Neues Passwort bestätigen", + "DefaultNameCopiedProfile": "{name} – Kopieren", + "AuthenticationMethod": "Authentifizierungsmethode", + "Clone": "Klonen", + "CountDownloadClientsSelected": "{count} Download-Client(s) ausgewählt", + "EditConnectionImplementation": "Verbindung bearbeiten - {implementationName}", + "EditDownloadClientImplementation": "Download Client bearbeiten - {implementationName}", + "IndexerTagsHelpTextWarning": "Tags sollten mit Vorsicht verwendet werden, da sie ungewollte Effekte haben können. Eine Anwendung mit einem Tag synchronisiert nur Indexer die den Gleichen Tag haben.", + "EditIndexerImplementation": "Indexer bearbeiten - {implementationName}", + "EditApplicationImplementation": "Anwendung hinzufügen - {implementationName}", + "EditIndexerProxyImplementation": "Indexer Proxy hinzufügen - {implementationName}", + "CountApplicationsSelected": "{count} Ausgewählte Sammlung(en)", + "DownloadClientAriaSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden", + "ManageClients": "Verwalte Clienten", + "BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert", + "DownloadClientSettingsInitialStateHelpText": "Anfangszustand für zu {clientName} hinzugefügte Torrents", + "DownloadClientSettingsDestinationHelpText": "Legt das Ziel für den Download manuell fest; lassen Sie es leer, um die Standardeinstellung zu verwenden", + "DownloadClientSettingsUrlBaseHelpText": "Fügt ein Präfix zur {clientName} Url hinzu, z.B. {url}", + "DownloadClientSettingsUseSslHelpText": "Sichere Verbindung verwenden, wenn Verbindung zu {clientName} hergestellt wird", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads; leer lassen, um den Standardspeicherort für Übertragungen zu verwenden", + "CustomFilter": "Benutzerdefinierter Filter", + "DownloadClientSettingsInitialState": "Ausgangszustand", + "Donate": "Spenden", + "DownloadClientFreeboxSettingsAppId": "App-ID", + "DownloadClientFreeboxSettingsAppIdHelpText": "App-ID, die beim Erstellen des Zugriffs auf die Freebox-API angegeben wird (z. B. „app_id“)", + "DownloadClientFreeboxSettingsPortHelpText": "Port, der für den Zugriff auf die Freebox-Schnittstelle verwendet wird, standardmäßig ist „{port}“", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Laden Sie zuerst das erste und das letzte Stück herunter (qBittorrent 4.1.0+)", + "IndexerBeyondHDSettingsSearchTypes": "Suchtyp", + "DownloadClientFloodSettingsUrlBaseHelpText": "Fügt der Flood-API ein Präfix hinzu, z. B. {url}", + "DownloadClientFreeboxSettingsApiUrl": "API-URL", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Definiere die Freebox-API-Basis-URL mit der API-Version, 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.", + "DownloadClientSettingsAddPaused": "Pausiert hinzufügen", + "SelectDownloadClientModalTitle": "{modalTitle} – Wähle Download-Client", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Erster und Letzter Erster", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Ausgangszustand für zu qBittorrent hinzugefügte Torrents. Beachten Sie, dass erzwungene Torrents nicht den Seed-Beschränkungen unterliegen", + "IndexerSettingsAppsMinimumSeeders": "Apps Mindestanzahl von Seedern", + "IndexerHDBitsSettingsMediums": "Medien", + "Destination": "Ziel", + "Directory": "Verzeichnis", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Fügt der Deluge-JSON-URL ein Präfix hinzu, siehe {url}", + "DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags", + "DownloadClientFloodSettingsTagsHelpText": "Erste Tags eines Downloads. Um erkannt zu werden, muss ein Download über alle Anfangs-Tags verfügen. Dies vermeidet Konflikte mit nicht verwandten Downloads.", + "DownloadClientFreeboxSettingsAppToken": "App-Token", + "DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „app_token“)", + "DownloadClientFreeboxSettingsHostHelpText": "Hostname oder Host-IP-Adresse der Freebox, standardmäßig „{url}“ (funktioniert nur im selben Netzwerk)", + "DownloadClientPneumaticSettingsNzbFolder": "NZB-Ordner", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Dieser Ordner muss über XBMC erreichbar sein", + "DownloadClientPneumaticSettingsStrmFolder": "Strm-Ordner", + "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm-Dateien in diesem Ordner werden von der Drohne importiert", + "DownloadClientQbittorrentSettingsSequentialOrder": "Fortlaufende Reihenfolge", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "In sequentieller Reihenfolge herunterladen (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Verwenden Sie eine sichere Verbindung. Siehe Optionen -> Web-Benutzeroberfläche -> „HTTPS statt HTTP verwenden“ in qBittorrent.", + "DownloadClientRTorrentSettingsAddStopped": "Hinzufügen gestoppt", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Durch die Aktivierung werden Torrents und Magnete im gestoppten Zustand zu rTorrent hinzugefügt. Dadurch können Magnetdateien beschädigt werden.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Optionaler Speicherort für Downloads. Lassen Sie das Feld leer, um den standardmäßigen rTorrent-Speicherort zu verwenden", + "DownloadClientRTorrentSettingsUrlPath": "URL-Pfad", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerSettingsVipExpiration": "VIP Ablaufdatum", + "TorrentBlackholeSaveMagnetFilesHelpText": "Speichern Sie den Magnet-Link, wenn keine .torrent-Datei verfügbar ist (nur nützlich, wenn der Download-Client in einer Datei gespeicherte Magnete unterstützt)", + "TorrentBlackholeTorrentFolder": "Torrent-Ordner", + "UseSsl": "SSL verwenden", + "UsenetBlackholeNzbFolder": "NZB-Ordner", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Für Magnet-Links zu verwendende Erweiterung, standardmäßig „.magnet“.", + "XmlRpcPath": "XML-RPC-Pfad", + "ProxyValidationBadRequest": "Proxy konnte nicht getestet werden. StatusCode: {statusCode}", + "SecretToken": "Geheimer Token", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Optionaler freigegebener Ordner zum Ablegen von Downloads. Lassen Sie das Feld leer, um den Standardspeicherort der Download Station zu verwenden", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Fügt Eigenschaften von Medien als Tags hinzu. Hinweise sind Beispiele.", + "TorrentBlackholeSaveMagnetFiles": "Speicher Magnetdateien", + "TorrentBlackholeSaveMagnetFilesExtension": "Speicher die Magnet-Dateienerweiterung", + "Default": "Standard", + "GrabRelease": "Release holen", + "Script": "Skript", + "IndexerDownloadClientHealthCheckMessage": "Indexer mit ungültigen Download-Clients: {indexerNames}.", + "Any": "Beliebig", + "BuiltIn": "Eingebaut", + "PublishedDate": "Veröffentlichungsdatum", + "Redirected": "Umleiten", + "AllSearchResultsHiddenByFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet", + "DockerUpdater": "Aktualisieren Sie den Docker-Container, um das Update zu erhalten", + "Download": "Herunterladen", + "ErrorRestoringBackup": "Fehler beim Wiederherstellen der Sicherung", + "ExternalUpdater": "{appName} ist so konfiguriert, dass es einen externen Aktualisierungsmechanismus verwendet", + "NoEventsFound": "Keine Ereignisse gefunden", + "RestartReloadNote": "Hinweis: {appName} startet während des Wiederherstellungsvorgangs automatisch neu und lädt die Benutzeroberfläche neu.", + "TheLogLevelDefault": "Die Protokollebene ist standardmäßig auf „Info“ eingestellt und kann unter „Allgemeine Einstellungen“ (/settings/general) geändert werden.", + "UpdateAppDirectlyLoadError": "{appName} kann nicht direkt aktualisiert werden.", + "UpdaterLogFiles": "Updater-Protokolldateien", + "WouldYouLikeToRestoreBackup": "Willst du das Backup '{name}' wiederherstellen?", + "AptUpdater": "Verwenden Sie apt, um das Update zu installieren", + "InstallLatest": "Neueste Version installieren", + "CurrentlyInstalled": "Derzeit installiert", + "Mixed": "Gemischt", + "DownloadClientQbittorrentSettingsContentLayout": "Inhaltslayout", + "FailedToFetchSettings": "Einstellungen können nicht abgerufen werden", + "External": "Extern", + "FailedToFetchUpdates": "Updates konnten nicht abgerufen werden", + "IndexerSettingsSeedRatio": "Seed-Verhältnis", + "Install": "Installieren", + "ManualGrab": "Manuelles Greifen", + "OverrideGrabModalTitle": "Überschreiben und Abrufen - {title}", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Ob das konfigurierte Inhaltslayout von qBittorrent, das ursprüngliche Layout des Torrents oder immer ein Unterordner erstellt werden soll (qBittorrent 4.3.2+)", + "HealthMessagesInfoBox": "Weitere Informationen zur Ursache dieser Gesundheitsprüfungsnachrichten findest du, indem du auf den Wiki-Link (Buch-Symbol) am Ende der Zeile klickst oder deine [Protokolle]({link}) überprüfst. Wenn du Schwierigkeiten hast, diese Nachrichten zu interpretieren, kannst du unseren Support kontaktieren, über die Links unten.", + "IndexerHDBitsSettingsMediumsHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "InvalidUILanguage": "Die UI ist auf eine ungültige Sprache eingestellt, korrigiere sie und speichere die Einstellungen", + "LogFilesLocation": "Protokolldateien befinden sich unter: {location}", + "Logout": "Abmelden", + "NoHistoryFound": "Keine Historie gefunden", + "PasswordConfirmation": "Passwortbestätigung", + "InfoUrl": "Info-URL", + "LogSizeLimit": "Protokollgrößenlimit", + "LogSizeLimitHelpText": "Maximale Protokolldateigröße in MB, bevor archiviert wird. Standard ist 1MB.", + "NotificationsEmailSettingsUseEncryption": "Verschlüsselung verwenden", + "NotificationsTelegramSettingsIncludeAppName": "{appName} im Titel einfügen", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optional den Nachrichtentitel mit {appName} voranstellen, um Benachrichtigungen von verschiedenen Anwendungen zu unterscheiden", + "LabelIsRequired": "Label ist erforderlich", + "Menu": "Menü", + "IndexerHDBitsSettingsCodecsHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsSeedTime": "Seed-Zeit", + "IndexerSettingsAdditionalParameters": "Zusätzliche Parameter", + "IndexerSettingsApiPath": "API-Pfad", + "IndexerSettingsApiPathHelpText": "Pfad zur API, normalerweise {url}", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Fügt der {clientName}-rpc-URL ein Präfix hinzu, z. B. {url}, standardmäßig '{defaultUrl}'", + "IndexerSettingsSeedRatioHelpText": "Das Verhältnis, das ein Torrent erreichen muss, bevor er gestoppt wird. Leer verwendet das Standardverhältnis des Download-Clients. Das Verhältnis sollte mindestens 1,0 betragen und den Regeln des Indexers folgen.", + "IndexerSettingsSeedTimeHelpText": "Die Zeit, die ein Torrent gesät werden sollte, bevor er gestoppt wird. Leer verwendet die Standardzeit des Download-Clients", + "InstallMajorVersionUpdate": "Update installieren", + "InstallMajorVersionUpdateMessage": "Dieses Update wird eine neue Hauptversion installieren und ist möglicherweise nicht mit deinem System kompatibel. Bist du sicher, dass du dieses Update installieren möchtest?", + "InstallMajorVersionUpdateMessageLink": "Weitere Informationen findest du unter [{domain}]({url}).", + "NotificationsEmailSettingsUseEncryptionHelpText": "Ob bevorzugt Verschlüsselung verwendet werden soll, wenn auf dem Server konfiguriert, ob immer Verschlüsselung über SSL (nur Port 465) oder StartTLS (anderer Port) verwendet wird oder keine Verschlüsselung verwendet wird", + "PackageVersionInfo": "{packageVersion} von {packageAuthor}", + "PreviouslyInstalled": "Früher installiert", + "PrioritySettings": "Priorität: {priority}", + "SeedRatio": "Seed-Verhältnis", + "SeedTime": "Seed-Zeit", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Wenn ein Torrent durch einen Hash blockiert wird, wird er möglicherweise nicht korrekt abgelehnt während RSS/Recherche für einige Indexer. Diese Option aktiviert die Ablehnung des Torrents nach dem Abrufen, aber bevor er an den Client gesendet wird.", + "IndexerHDBitsSettingsOriginsHelpText": "Wenn nicht angegeben, werden alle Optionen verwendet.", + "IndexerSettingsGrabLimit": "Grab-Limit", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Nur nach Freeleech suchen (Begrenztes UL)", + "IndexerFileListSettingsPasskeyHelpText": "Site Passkey (Dies ist die alphanumerische Zeichenfolge in der Tracker-URL, die in deinem Download-Client angezeigt wird)", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie-Benutzer-Agent", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Standard-Fallback-Kategorie, wenn für eine Veröffentlichung keine zugeordnete Kategorie existiert. Das Hinzufügen einer für {appName} spezifischen Kategorie vermeidet Konflikte mit nicht verwandten {appName}-Downloads. Die Verwendung einer Kategorie ist optional, aber dringend empfohlen. Eine [Kategorie]-Unterverzeichnis wird im Ausgabeverzeichnis erstellt.", + "IndexerBeyondHDSettingsRssKeyHelpText": "RSS-Schlüssel von der Website (zu finden unter Mein Sicherheitsbereich => RSS-Schlüssel)", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Aktiviere diese Option, wenn du Torrent-Dateinamen als Releasetitel verwenden möchtest", + "IndexerSettingsAppsMinimumSeedersHelpText": "Minimale benötigte Seeder von den Anwendungen, damit der Indexer greifen kann; leer ist die Standardeinstellung des Sync-Profils", + "IndexerGazelleGamesSettingsApiKeyHelpText": "API-Schlüssel von der Seite (Zu finden unter Einstellungen => Zugriffseinstellungen)", + "IndexerPassThePopcornSettingsApiUserHelpText": "Diese Einstellungen findest du in deinen PassThePopcorn-Sicherheitseinstellungen (Profil bearbeiten > Sicherheit).", + "IndexerSettingsQueryLimitHelpText": "Die maximale Anzahl an Queries, die {appName} der Seite gemäß der jeweiligen Einheit erlauben wird", + "IndexerRedactedSettingsApiKeyHelpText": "API-Schlüssel von der Seite (Zu finden unter Einstellungen => Zugriffseinstellungen)", + "IndexerSettingsQueryLimit": "Query Limit", + "PackSeedTimeHelpText": "Die Zeit, die ein Pack (Season oder Diskographie)-Torrent gesät werden soll, bevor er gestoppt wird. Leer ist die Standardeinstellung der App", + "TotalIndexerSuccessfulGrabs": "Gesamtanzahl erfolgreicher Indexer-Suchanfragen", + "ProwlarrDownloadClientsInAppOnlyAlert": "Download-Clients sind nur für In-App-Suchen in {appName} und synchronisieren sich nicht mit Apps. Es sind keine Pläne vorgesehen, eine solche Funktionalität hinzuzufügen.", + "TotalUserAgentGrabs": "Gesamtanzahl der User-Agent-Grabs", + "DefaultCategory": "Standardkategorie", + "IndexerDownloadClientHelpText": "Gib an, welcher Download-Client für Grab-Vorgänge, die innerhalb von {appName} von diesem Indexer durchgeführt werden, verwendet wird", + "IndexerHistoryLoadError": "Fehler beim Laden der Indexer-Historie", + "IndexerNzbIndexSettingsApiKeyHelpText": "Website-API-Key", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Website-API-Key", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Sucher nur Golden Popcorn Releases", + "IndexerSettingsCookieHelpText": "Website Cookie", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Die Zeit, die ein Pack (Season oder Diskographie)-Torrent gesät werden soll, bevor er gestoppt wird. Leer ist die Standardeinstellung der App", + "SearchAllIndexers": "Alle Indexer durchsuchen", + "SearchCountIndexers": "Suche {count} Indexer(s)", + "SeedTimeHelpText": "Die Zeit, die ein Torrent gesät werden soll, bevor er gestoppt wird. Leer ist die Standardeinstellung der App", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Suche nur Freeleech-Releases", + "IndexerNewznabSettingsVipExpirationHelpText": "Gib das Datum (yyyy-mm-dd) für das VIP-Ablaufdatum ein oder lasse es leer, {appName} benachrichtigt eine Woche vor Ablauf des VIP", + "ProxyValidationUnableToConnect": "Kann nicht mit dem Proxy verbunden werden: {exceptionMessage}. Überprüfe das Protokoll rund um diesen Fehler für Details", + "IndexerId": "Indexer ID", + "OnGrabHelpText": "Bei Release Grab", + "AuthQueries": "Authentifizierungsanfragen", + "PackSeedTime": "Pack-Seed-Zeit", + "DeleteSelectedApplications": "Ausgewählte Anwendungen löschen", + "DownloadClientSettingsDefaultCategoryHelpText": "Standard-Fallback-Kategorie, wenn für eine Veröffentlichung keine zugeordnete Kategorie existiert. Das Hinzufügen einer für {appName} spezifischen Kategorie vermeidet Konflikte mit nicht verwandten {appName}-Downloads. Die Verwendung einer Kategorie ist optional, aber dringend empfohlen.", + "FoundCountReleases": "{itemCount} Veröffentlichungen gefunden", + "IncludeManualGrabsHelpText": "Manuelle Abrufe, die innerhalb von {appName} gemacht wurden, einbeziehen", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Nur nach Freeleech-Veröffentlichungen suchen", + "IndexerBeyondHDSettingsApiKeyHelpText": "API-Schlüssel von der Website (zu finden in „Meine Sicherheit“ => „API-Schlüssel“)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Nur nach Freeleech-Releases suchen", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Nur nach Rückerstattungen suchen", + "IndexerDisabled": "Indexer deaktiviert", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Suche nur Freeleech-Releases", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Suche Veröffentlichungen nach Gruppennamen", + "IndexerNebulanceSettingsApiKeyHelpText": "API-Schlüssel aus den Benutzereinstellungen > API-Schlüssel. Der Schlüssel muss List- und Download-Berechtigungen haben", + "IndexerSettingsGrabLimitHelpText": "Die maximale Anzahl an Grabs, die {appName} der Seite erlauben wird, wie von der jeweiligen Einheit festgelegt", + "IndexerSettingsLimitsUnit": "Limits-Einheit", + "IndexerSettingsLimitsUnitHelpText": "Die Zeiteinheit zur Berechnung der Limits pro Indexer", + "IndexerStatus": "Indexer Status", + "LastFailure": "Letzter Fehler", + "ManageApplications": "Applikationen verwalten", + "NoApplicationsFound": "Keine Applikationen gefunden", + "NoIndexerCategories": "Keine Kategorien für diesen Indexer gefunden", + "NoIndexerHistory": "Keine Historie für diesen Indexer gefunden", + "Open": "Offen", + "OverrideAndAddToDownloadClient": "Überschreiben und zum Download-Client hinzufügen", + "PreferMagnetUrl": "Magnet URL bevorzugen", + "PreferMagnetUrlHelpText": "Wenn aktiviert, wird dieser Indexer die Verwendung von Magnet-URLs für Grabs bevorzugen, mit Rückfall auf Torrent-Links", + "RssQueries": "RSS Anfragen", + "TotalHostGrabs": "Gesamtanzahl der Host-Grabs", + "TotalHostQueries": "Gesamtanzahl der Host-Suchanfragen", + "SeedRatioHelpText": "Das Verhältnis, das ein Torrent erreichen sollte, bevor er gestoppt wird. Leer ist die Standardeinstellung der App", + "AverageGrabs": "Durchschnittliche Abrufe", + "AverageQueries": "Durchschnittliche Anfragen", + "SelectedCountOfCountReleases": "Ausgewählt {selectedCount} von {itemCount} Releases", + "NewznabUrl": "Newznab Url", + "QueryType": "Abfragetyp", + "DisabledUntil": "Deaktiviert bis", + "MappedCategories": "Zuordnete Kategorien", + "AreYouSureYouWantToDeleteIndexer": "Bist du sicher, dass du „{name}“ aus {appName} löschen möchtest?", + "TotalIndexerQueries": "Gesamtanzahl der Indexer-Suchanfragen", + "ProwlarrDownloadClientsAlert": "Wenn du beabsichtigst, direkt innerhalb von {appName} zu suchen, musst du Download-Clients hinzufügen. Andernfalls musst du sie hier nicht hinzufügen. Für Suchen aus deinen Apps werden stattdessen die dort konfigurierten Download-Clients verwendet.", + "AppsMinimumSeedersHelpText": "Mindestanzahl an Seedern, die von der Anwendung für den Indexer erforderlich ist, um herunterzuladen. Leer bedeutet, dass das Standardprofil der Synchronisierung verwendet wird", + "CountIndexersAvailable": "{count} Indexer verfügbar", + "DeleteClientCategory": "Download-Client-Kategorie löschen", + "DeleteSelectedIndexer": "Ausgewählten Indexer löschen", + "TotalGrabs": "Gesamtanzahl der Grabs", + "DownloadClientCategory": "Download-Client-Kategorie", + "EditCategory": "Kategorie bearbeiten", + "IndexerSettingsApiUser": "API Benutzer", + "RssFeed": "RSS Feed", + "InitialFailure": "Initialer Fehler", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Blockierte Torrent-Hashes beim Abrufen synchron ablehnen", + "DeleteSelectedIndexers": "Ausgewählte Indexer löschen", + "IndexerHDBitsSettingsPasskeyHelpText": "Passkey aus den Benutzerdetails", + "IndexerSettingsPasskey": "Pass Key", + "ClickToChangeQueryOptions": "Klicken, um Abfrageoptionen zu ändern", + "IndexerCategories": "Indexer-Kategorien", + "SearchQueries": "Suchanfragen", + "IndexerAlphaRatioSettingsExcludeScene": "SCENE ausschließen", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "SCENE-Veröffentlichungen aus den Ergebnissen ausschließen", + "IndexerBeyondHDSettingsRefundOnly": "Nur Rückerstattung", + "IndexerFileListSettingsUsernameHelpText": "Website-Benutzername", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Muss Benutzer- und Torrents-Berechtigungen haben", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Zeige nur Freeleech-Releases", + "IndexerHDBitsSettingsUsernameHelpText": "Webseite-Benutzername", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Suche nur Freeleech-Releases", + "IndexerNewznabSettingsApiKeyHelpText": "Website API Key", + "IndexerOrpheusSettingsApiKeyHelpText": "API-Schlüssel von der Seite (Zu finden unter Einstellungen => Zugriffseinstellungen)", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Suche nur Freeleech-Releases", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Website-API-Schlüssel", + "AreYouSureYouWantToDeleteCategory": "Bist du sicher, dass du die zugeordnete Kategorie löschen möchtest?", + "DownloadClientSettingsPriorityItemHelpText": "Priorität, die beim Abrufen von Elementen verwendet werden soll", + "GoToApplication": "Zur Anwendung gehen", + "HistoryDetails": "Historie-Details", + "IndexerBeyondHDSettingsLimitedOnly": "Nur begrenzt", + "IndexerHDBitsSettingsOrigins": "Ursprünge", + "IndexerHDBitsSettingsUseFilenames": "Verwende Dateinamen", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent, der mit dem Cookie aus dem Browser verwendet wird", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Zusätzliche Newznab-Parameter", + "IndexerSettingsPackSeedTime": "Pack-Seed-Zeit", + "IndexerSettingsRssKey": "RSS Schlüssel", + "IndexerMTeamTpSettingsApiKeyHelpText": "API-Schlüssel von der Seite (Zu finden im Benutzersteuerungsfeld => Sicherheit => Labor)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Suche nur Freeleech-Releases", + "IndexerSettingsFreeleechOnly": "Nur Freeleech", + "IndexerSettingsPreferMagnetUrl": "Magnet-URL bevorzugen", + "IndexerSettingsPreferMagnetUrlHelpText": "Wenn aktiviert, bevorzugt dieser Indexer die Verwendung von Magnet-URLs für Grabs mit Rückfall auf Torrent-Links", + "TorznabUrl": "Torznab Url", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Nur Golden Popcorn", + "IndexerSettingsBaseUrl": "Basis Url", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Nur nach Freeleech-Veröffentlichungen suchen", + "IndexerAvistazSettingsPasswordHelpText": "Website-Passwort", + "IndexerAvistazSettingsPidHelpText": "PID aus der „Mein Konto“- oder „Mein Profil“-Seite", + "IndexerAvistazSettingsUsernameHelpText": "Website-Benutzername", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Nur Mitglieder mit Rang „Mitglied“ und höher können die API auf diesem Indexer nutzen.", + "IndexerBeyondHDSettingsRewindOnly": "Nur zurückspulen", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Nur Rückwärtssuche", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Wähle die Arten von Veröffentlichungen aus, die dich interessieren. Wenn keine ausgewählt sind, werden alle Optionen verwendet.", + "IndexerFailureRate": "Indexer-Fehlerrate", + "IndexerGazelleGamesSettingsSearchGroupNames": "Suche Gruppennamen", + "IndexerSettingsBaseUrlHelpText": "Wähle die Basis-Url aus, die {appName} für Anfragen an die Seite verwenden soll", + "RepeatSearch": "Suche wiederholen", + "AverageResponseTimesMs": "Durchschnittliche Indexer-Antwortzeiten (ms)", + "BasicSearch": "Einfache Suche" } diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 05c1a5225..4c6187fcc 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -28,8 +28,8 @@ "EventType": "Είδος Γεγονότος", "Events": "Γεγονότα", "Edit": "Επεξεργασία", - "DownloadClientStatusCheckSingleClientMessage": "Προγράμματα λήψης που είναι μη διαθέσιμα λόγων αποτυχιών: {0}", - "DownloadClientStatusCheckAllClientMessage": "Όλα τα προγράμματα λήψης είναι μη διαθέσιμα λόγων αποτυχιών", + "DownloadClientStatusSingleClientHealthCheckMessage": "Προγράμματα λήψης που είναι μη διαθέσιμα λόγων αποτυχιών: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Όλα τα προγράμματα λήψης είναι μη διαθέσιμα λόγων αποτυχιών", "DownloadClientsSettingsSummary": "Κάντε λήψη της διαμόρφωσης πελατών για ενσωμάτωση στην αναζήτηση διεπαφής χρήστη {appName}", "CustomFilters": "Custom Φιλτρα", "ConnectSettingsSummary": "Ειδοποιήσεις και προσαρμοσμένα σενάρια", @@ -45,7 +45,7 @@ "Indexer": "Ευρετήριο", "PendingChangesDiscardChanges": "Απορρίψτε τις αλλαγές και φύγετε", "ShowSearchHelpText": "Εμφάνιση κουμπιού αναζήτησης στο δείκτη", - "UpdateCheckStartupNotWritableMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος εκκίνησης \"{0}\" δεν είναι εγγράψιμος από τον χρήστη \"{1}\".", + "UpdateStartupNotWritableHealthCheckMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος εκκίνησης \"{startupFolder}\" δεν είναι εγγράψιμος από τον χρήστη \"{userName}\".", "BranchUpdateMechanism": "Υποκατάστημα που χρησιμοποιείται από εξωτερικό μηχανισμό ενημέρωσης", "Mode": "Τρόπος", "SettingsEnableColorImpairedMode": "Ενεργοποίηση λειτουργίας με προβλήματα χρώματος", @@ -60,8 +60,8 @@ "Reddit": "Reddit", "Result": "Αποτέλεσμα", "Retention": "Κράτηση", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "Το RSS δεν υποστηρίζεται με αυτό το ευρετήριο", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "Το RSS δεν υποστηρίζεται με αυτό το ευρετήριο", "Save": "Σώσει", "SaveChanges": "Αποθήκευσε τις αλλαγές", "SettingsEnableColorImpairedModeHelpText": "Τροποποιημένο στυλ για να επιτρέπεται στους χρήστες με προβλήματα χρώματος να διακρίνουν καλύτερα τις πληροφορίες με χρωματική κωδικοποίηση", @@ -73,8 +73,8 @@ "NoChange": "Καμία αλλαγή", "Port": "Λιμάνι", "PortNumber": "Αριθμός θύρας", - "IndexerStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών", - "IndexerStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών", + "IndexerStatusUnavailableHealthCheckMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών: {indexerNames}", "KeyboardShortcuts": "Συντομεύσεις πληκτρολογίου", "Language": "Γλώσσα", "Reset": "Επαναφορά", @@ -113,7 +113,7 @@ "ProxyBypassFilterHelpText": "Χρησιμοποιήστε το \",\" ως διαχωριστικό και \"*.\" ως μπαλαντέρ για υποτομείς", "UnableToAddANewAppProfilePleaseTryAgain": "Δεν είναι δυνατή η προσθήκη ενός νέου προφίλ ποιότητας. Δοκιμάστε ξανά.", "UnableToLoadHistory": "Δεν είναι δυνατή η φόρτωση του ιστορικού", - "UpdateCheckUINotWritableMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος διεπαφής χρήστη \"{0}\" δεν είναι εγγράψιμος από τον χρήστη \"{1}\".", + "UpdateUiNotWritableHealthCheckMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος διεπαφής χρήστη \"{uiFolder}\" δεν είναι εγγράψιμος από τον χρήστη \"{userName}\".", "AuthenticationMethodHelpText": "Απαιτήστε όνομα χρήστη και κωδικό πρόσβασης για πρόσβαση στο {appName}", "Automatic": "Αυτόματο", "BeforeUpdate": "Πριν από την ενημέρωση", @@ -122,7 +122,7 @@ "ChangeHasNotBeenSavedYet": "Η αλλαγή δεν έχει αποθηκευτεί ακόμα", "CloneProfile": "Προφίλ κλώνου", "CloseCurrentModal": "Κλείσιμο τρέχοντος modal", - "DBMigration": "Μετεγκατάσταση DB", + "DatabaseMigration": "Μετεγκατάσταση DB", "DeleteApplicationMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την ειδοποίηση \"{0}\";", "DeleteBackupMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας \"{0}\";", "DeleteIndexerProxyMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη λίστα \"{0}\";", @@ -135,14 +135,14 @@ "ErrorLoadingContents": "Σφάλμα κατά τη φόρτωση περιεχομένων", "GeneralSettings": "Γενικές Ρυθμίσεις", "Grabs": "Αρπάζω", - "HealthNoIssues": "Δεν υπάρχουν προβλήματα με τη διαμόρφωσή σας", + "NoIssuesWithYourConfiguration": "Δεν υπάρχουν προβλήματα με τη διαμόρφωσή σας", "HomePage": "Αρχική σελίδα", "Host": "Πλήθος", "Hostname": "Όνομα κεντρικού υπολογιστή", - "IndexerLongTermStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών για περισσότερο από 6 ώρες: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών για περισσότερο από 6 ώρες: {indexerNames}", "IndexerPriorityHelpText": "Προτεραιότητα ευρετηρίου από 1 (Υψηλότερη) έως 50 (Χαμηλότερη). Προεπιλογή: 25.", - "IndexerProxyStatusCheckAllClientMessage": "Όλες οι λίστες δεν είναι διαθέσιμες λόγω αστοχιών", - "IndexerProxyStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Όλες οι λίστες δεν είναι διαθέσιμες λόγω αστοχιών", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών: {indexerProxyNames}", "LaunchBrowserHelpText": " Ανοίξτε ένα πρόγραμμα περιήγησης ιστού και μεταβείτε στην αρχική σελίδα του {appName} κατά την έναρξη της εφαρμογής.", "LogFiles": "Αρχεία καταγραφής", "Logging": "Ξύλευση", @@ -166,8 +166,8 @@ "PendingChangesStayReview": "Παραμείνετε και ελέγξτε τις αλλαγές", "Presets": "Προεπιλογές", "Priority": "Προτεραιότητα", - "ProxyCheckFailedToTestMessage": "Αποτυχία δοκιμής διακομιστή μεσολάβησης: {0}", - "ProxyCheckResolveIpMessage": "Αποτυχία επίλυσης της διεύθυνσης IP για τον Διαμορφωμένο διακομιστή μεσολάβησης {0}", + "ProxyFailedToTestHealthCheckMessage": "Αποτυχία δοκιμής διακομιστή μεσολάβησης: {url}", + "ProxyResolveIpHealthCheckMessage": "Αποτυχία επίλυσης της διεύθυνσης IP για τον Διαμορφωμένο διακομιστή μεσολάβησης {proxyHostName}", "Queue": "Ουρά", "ReadTheWikiForMoreInformation": "Διαβάστε το Wiki για περισσότερες πληροφορίες", "Refresh": "Φρεσκάρω", @@ -196,7 +196,7 @@ "SSLCertPath": "Διαδρομή πιστοποίησης SSL", "StartTypingOrSelectAPathBelow": "Ξεκινήστε να πληκτρολογείτε ή επιλέξτε μια διαδρομή παρακάτω", "Style": "Στυλ", - "SystemTimeCheckMessage": "Ο χρόνος συστήματος είναι απενεργοποιημένος για περισσότερο από 1 ημέρα. Οι προγραμματισμένες εργασίες ενδέχεται να μην εκτελούνται σωστά έως ότου διορθωθεί η ώρα", + "SystemTimeHealthCheckMessage": "Ο χρόνος συστήματος είναι απενεργοποιημένος για περισσότερο από 1 ημέρα. Οι προγραμματισμένες εργασίες ενδέχεται να μην εκτελούνται σωστά έως ότου διορθωθεί η ώρα", "TableOptions": "Επιλογές πίνακα", "TableOptionsColumnsMessage": "Επιλέξτε ποιες στήλες είναι ορατές και με ποια σειρά εμφανίζονται", "TagIsNotUsedAndCanBeDeleted": "Η ετικέτα δεν χρησιμοποιείται και μπορεί να διαγραφεί", @@ -219,7 +219,7 @@ "UnableToLoadNotifications": "Δεν είναι δυνατή η φόρτωση ειδοποιήσεων", "UnableToLoadUISettings": "Δεν είναι δυνατή η φόρτωση των ρυθμίσεων διεπαφής χρήστη", "UnsavedChanges": "Μη αποθηκευμένες αλλαγές", - "UpdateCheckStartupTranslocationMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος εκκίνησης \"{0}\" βρίσκεται σε ένα φάκελο \"Μετατόπιση εφαρμογών\".", + "UpdateStartupTranslocationHealthCheckMessage": "Δεν είναι δυνατή η εγκατάσταση της ενημέρωσης επειδή ο φάκελος εκκίνησης \"{startupFolder}\" βρίσκεται σε ένα φάκελο \"Μετατόπιση εφαρμογών\".", "UpdateScriptPathHelpText": "Διαδρομή σε ένα προσαρμοσμένο σενάριο που λαμβάνει ένα εξαγόμενο πακέτο ενημέρωσης και χειρίζεται το υπόλοιπο της διαδικασίας ενημέρωσης", "URLBase": "Βάση διεύθυνσης URL", "UrlBaseHelpText": "Για αντίστροφη υποστήριξη διακομιστή μεσολάβησης, η προεπιλογή είναι άδεια", @@ -234,18 +234,18 @@ "IncludeHealthWarningsHelpText": "Συμπεριλάβετε προειδοποιήσεις για την υγεία", "Security": "Ασφάλεια", "Tasks": "Καθήκοντα", - "UnableToLoadBackups": "Δεν είναι δυνατή η φόρτωση αντιγράφων ασφαλείας", - "UnableToLoadDownloadClients": "Δεν είναι δυνατή η φόρτωση πελατών λήψης", + "BackupsLoadError": "Δεν είναι δυνατή η φόρτωση αντιγράφων ασφαλείας", + "DownloadClientsLoadError": "Δεν είναι δυνατή η φόρτωση πελατών λήψης", "UpdateMechanismHelpText": "Χρησιμοποιήστε το ενσωματωμένο πρόγραμμα ενημέρωσης του {appName} ή ένα script", "AnalyticsEnabledHelpText": "Στείλτε ανώνυμες πληροφορίες χρήσης και σφάλματος στους διακομιστές του {appName}. Αυτό περιλαμβάνει πληροφορίες στο πρόγραμμα περιήγησής σας, ποιες σελίδες {appName} WebUI χρησιμοποιείτε, αναφορά σφαλμάτων καθώς και έκδοση λειτουργικού συστήματος και χρόνου εκτέλεσης. Θα χρησιμοποιήσουμε αυτές τις πληροφορίες για να δώσουμε προτεραιότητα σε λειτουργίες και διορθώσεις σφαλμάτων.", "AppDataDirectory": "Κατάλογος AppData", "BindAddress": "Δεσμευμένη διεύθυνση", "EnableRss": "Ενεργοποίηση RSS", "IndexerFlags": "Σημαίες ευρετηρίου", - "IndexerLongTermStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών για περισσότερο από 6 ώρες", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών για περισσότερο από 6 ώρες", "InteractiveSearch": "Διαδραστική αναζήτηση", "Interval": "Διάστημα", - "ProxyCheckBadRequestMessage": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {0}", + "ProxyBadRequestHealthCheckMessage": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}", "ProxyPasswordHelpText": "Πρέπει να εισαγάγετε ένα όνομα χρήστη και έναν κωδικό πρόσβασης μόνο εάν απαιτείται. Αφήστε τα κενά διαφορετικά.", "ProxyType": "Τύπος διακομιστή μεσολάβησης", "ProxyUsernameHelpText": "Πρέπει να εισαγάγετε ένα όνομα χρήστη και έναν κωδικό πρόσβασης μόνο εάν απαιτείται. Αφήστε τα κενά διαφορετικά.", @@ -356,14 +356,14 @@ "Auth": "Auth", "BookSearch": "Αναζήτηση βιβλίου", "FullSync": "Πλήρης συγχρονισμός", - "IndexerVipCheckExpiringClientMessage": "Τα οφέλη VIP του ευρετηρίου λήγουν σύντομα: {0}", + "IndexerVipExpiringHealthCheckMessage": "Τα οφέλη VIP του ευρετηρίου λήγουν σύντομα: {indexerNames}", "NotSupported": "Δεν υποστηρίζεται", "Parameters": "Παράμετροι", "Public": "Δημόσιο", "QueryOptions": "Επιλογές ερωτήματος", "SearchIndexers": "Αναζήτηση ευρετηρίων", "SearchType": "Τύπος αναζήτησης", - "UnableToLoadApplicationList": "Δεν είναι δυνατή η φόρτωση της λίστας εφαρμογών", + "ApplicationsLoadError": "Δεν είναι δυνατή η φόρτωση της λίστας εφαρμογών", "AddRemoveOnly": "Μόνο προσθήκη και αφαίρεση", "ProwlarrSupportsAnyDownloadClient": "Το {appName} υποστηρίζει οποιοδήποτε από τα προγράμματα-πελάτες λήψης που αναφέρονται παρακάτω.", "Query": "Ερώτηση", @@ -394,7 +394,7 @@ "IndexerQuery": "Ερώτημα ευρετηρίου", "IndexerSettingsSummary": "Διαμορφώστε διάφορες καθολικές ρυθμίσεις ευρετηρίου, συμπεριλαμβανομένων των διακομιστών μεσολάβησης.", "IndexerSite": "Ιστότοπος ευρετηρίου", - "IndexerVipCheckExpiredClientMessage": "Τα προνόμια VIP του ευρετηρίου έχουν λήξει: {0}", + "IndexerVipExpiredHealthCheckMessage": "Τα προνόμια VIP του ευρετηρίου έχουν λήξει: {indexerNames}", "MappedCategories": "Χαρτογραφημένες κατηγορίες", "MovieSearch": "Αναζήτηση ταινίας", "MovieSearchTypes": "Τύποι αναζήτησης ταινιών", @@ -431,7 +431,7 @@ "IndexerInfo": "Πληροφορίες ευρετηρίου", "IndexerName": "Όνομα ευρετηρίου", "IndexerProxies": "Proxer Indexer", - "IndexerNoDefCheckMessage": "Τα ευρετήρια δεν έχουν ορισμό και δεν θα λειτουργήσουν: {0}. Αφαιρέστε και (ή) προσθέστε ξανά στο {appName}", + "IndexerNoDefinitionCheckHealthCheckMessage": "Τα ευρετήρια δεν έχουν ορισμό και δεν θα λειτουργήσουν: {indexerNames}. Αφαιρέστε και (ή) προσθέστε ξανά στο {appName}", "SemiPrivate": "Ημι-ιδιωτικό", "SettingsIndexerLoggingHelpText": "Καταγραφή πρόσθετων δεδομένων ευρετηρίου συμπεριλαμβανομένης της απόκρισης", "SearchTypes": "Τύποι αναζήτησης", @@ -458,8 +458,8 @@ "SyncLevelFull": "Πλήρης συγχρονισμός: Θα διατηρήσει πλήρως συγχρονισμένα τα ευρετήρια αυτής της εφαρμογής. Στη συνέχεια, οι αλλαγές που γίνονται στους indexers στο {appName} συγχρονίζονται με αυτήν την εφαρμογή. Οποιαδήποτε αλλαγή γίνει σε ευρετήρια απομακρυσμένα σε αυτήν την εφαρμογή θα παρακαμφθεί από τον {appName} στον επόμενο συγχρονισμό.", "Remove": "Αφαιρώ", "Replace": "Αντικαθιστώ", - "TheLatestVersionIsAlreadyInstalled": "Η τελευταία έκδοση του {appName} είναι ήδη εγκατεστημένη", - "ApiKeyValidationHealthCheckMessage": "Παρακαλούμε ενημερώστε το κλείδι API ώστε να έχει τουλάχιστον {0} χαρακτήρες. Μπορείτε να το κάνετε αυτό μέσα από τις ρυθμίσεις ή το αρχείο ρυθμίσεων", + "OnLatestVersion": "Η τελευταία έκδοση του {appName} είναι ήδη εγκατεστημένη", + "ApiKeyValidationHealthCheckMessage": "Παρακαλούμε ενημερώστε το κλείδι API ώστε να έχει τουλάχιστον {length} χαρακτήρες. Μπορείτε να το κάνετε αυτό μέσα από τις ρυθμίσεις ή το αρχείο ρυθμίσεων", "StopSelecting": "Διακοπή Επιλογής", "OnHealthRestored": "Στην Αποκατάσταση Υγείας", "ApplicationURL": "Διεύθυνση URL εφαρμογής", @@ -485,7 +485,7 @@ "Theme": "Θέμα", "Track": "Ιχνος", "Year": "Ετος", - "UpdateAvailable": "Νέα ενημέρωση είναι διαθέσιμη", + "UpdateAvailableHealthCheckMessage": "Νέα ενημέρωση είναι διαθέσιμη", "Artist": "Καλλιτέχνης", "Author": "Συγγραφέας", "Book": "Βιβλίο", @@ -501,7 +501,7 @@ "DeleteAppProfileMessageText": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το προφίλ ποιότητας '{0}'?", "AddConnection": "Προσθήκη Σύνδεσης", "NotificationStatusAllClientHealthCheckMessage": "Όλες οι λίστες δεν είναι διαθέσιμες λόγω αστοχιών", - "NotificationStatusSingleClientHealthCheckMessage": "Μη διαθέσιμες λίστες λόγω αποτυχιών: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Μη διαθέσιμες λίστες λόγω αποτυχιών: {notificationNames}", "AuthBasic": "Βασικό (Αναδυόμενο παράθυρο προγράμματος περιήγησης)", "AuthForm": "Φόρμες (σελίδα σύνδεσης)", "Clone": "Κλωνοποίηση", @@ -515,7 +515,30 @@ "EditConnectionImplementation": "Προσθήκη", "EditApplicationImplementation": "Προσθήκη", "AddApplicationImplementation": "Προσθήκη", - "AddConnectionImplementation": "Προσθήκη", + "AddConnectionImplementation": "Προσθήκη - {implementationName}", "AddIndexerImplementation": "Προσθήκη", - "EditIndexerProxyImplementation": "Προσθήκη" + "EditIndexerProxyImplementation": "Προσθήκη", + "CountApplicationsSelected": "Επιλέχθηκαν {0} συλλογές", + "IndexerBeyondHDSettingsSearchTypes": "Τύποι αναζήτησης", + "IndexerHDBitsSettingsMediums": "Μεσαίο", + "UseSsl": "Χρησιμοποιήστε SSL", + "CustomFilter": "Custom Φιλτρα", + "GrabRelease": "Πιάσε την απελευθέρωση", + "ProxyValidationBadRequest": "Αποτυχία δοκιμής διακομιστή μεσολάβησης. StatusCode: {statusCode}", + "Script": "Γραφή", + "BuiltIn": "Ενσωματωμένο", + "PublishedDate": "Ημερομηνία δημοσίευσης", + "Redirected": "Διευθύνω πάλιν", + "AllSearchResultsHiddenByFilter": "Όλα τα αποτελέσματα αποκρύπτονται από το εφαρμοσμένο φίλτρο", + "Download": "Κατεβάστε", + "ErrorRestoringBackup": "Σφάλμα κατά την επαναφορά του αντιγράφου ασφαλείας", + "ExternalUpdater": "Το {appName} έχει ρυθμιστεί να χρησιμοποιεί έναν εξωτερικό μηχανισμό ενημέρωσης", + "NoEventsFound": "Δεν βρέθηκαν συμβάντα", + "RestartReloadNote": "Σημείωση: Το {appName} θα επανεκκινήσει αυτόματα και θα φορτώσει ξανά το περιβάλλον εργασίας χρήστη κατά τη διαδικασία επαναφοράς.", + "UpdateAppDirectlyLoadError": "Δεν είναι δυνατή η απευθείας ενημέρωση του {appName},", + "DockerUpdater": "ενημερώστε το κοντέινερ για να λάβετε την ενημέρωση", + "AptUpdater": "Χρησιμοποιήστε το apt για να εγκαταστήσετε την ενημέρωση", + "InstallLatest": "Εγκατάσταση πιο πρόσφατου", + "CurrentlyInstalled": "Εγκατεστημένο αυτήν τη στιγμή", + "Mixed": "Σταθερός" } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 2b4176417..2565fdf01 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -31,11 +31,13 @@ "Album": "Album", "All": "All", "AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.", + "AllSearchResultsHiddenByFilter": "All search results are hidden by the applied filter.", "Analytics": "Analytics", "AnalyticsEnabledHelpText": "Send anonymous usage and error information to {appName}'s servers. This includes information on your browser, which {appName} WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.", + "Any": "Any", "ApiKey": "API Key", - "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", - "AppDataDirectory": "AppData directory", + "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file", + "AppDataDirectory": "AppData Directory", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppProfileInUse": "App Profile in Use", "AppProfileSelectHelpText": "App profiles are used to control RSS, Automatic Search and Interactive Search settings on application sync", @@ -45,13 +47,16 @@ "Application": "Application", "ApplicationLongTermStatusCheckAllClientMessage": "All applications are unavailable due to failures for more than 6 hours", "ApplicationLongTermStatusCheckSingleClientMessage": "Applications unavailable due to failures for more than 6 hours: {0}", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sync Reject Blocklisted Torrent Hashes While Grabbing", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", "ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures", "ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}", - "ApplicationTagsHelpText": "Sync Indexers to this application that have no tags or that have 1 or more matching tags", + "ApplicationTagsHelpText": "Sync Indexers to this application that have one or more matching tags. If no tags are listed here, then no indexers will be prevented from syncing due to their tags.", "ApplicationTagsHelpTextWarning": "Tags should be used with caution, they can have unintended effects. An app with a tag will only sync with indexers having the same tag.", "ApplicationURL": "Application URL", "ApplicationUrlHelpText": "This application's external URL including http(s)://, port and URL base", "Applications": "Applications", + "ApplicationsLoadError": "Unable to load application list", "Apply": "Apply", "ApplyChanges": "Apply Changes", "ApplyTags": "Apply Tags", @@ -63,6 +68,7 @@ "Apps": "Apps", "AppsMinimumSeeders": "Apps Minimum Seeders", "AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default", + "AptUpdater": "Use apt to install the update", "AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?", "AreYouSureYouWantToDeleteIndexer": "Are you sure you want to delete '{name}' from {appName}?", "Artist": "Artist", @@ -84,6 +90,8 @@ "Author": "Author", "Automatic": "Automatic", "AutomaticSearch": "Automatic Search", + "AverageGrabs": "Average Grabs", + "AverageQueries": "Average Queries", "AverageResponseTimesMs": "Average Indexer Response Times (ms)", "Backup": "Backup", "BackupFolderHelpText": "Relative paths will be under {appName}'s AppData directory", @@ -91,16 +99,19 @@ "BackupNow": "Backup Now", "BackupRetentionHelpText": "Automatic backups older than the retention period will be cleaned up automatically", "Backups": "Backups", + "BackupsLoadError": "Unable to load backups", "BasicSearch": "Basic Search", "BeforeUpdate": "Before update", "BindAddress": "Bind Address", "BindAddressHelpText": "Valid IP address, localhost or '*' for all interfaces", + "BlackholeFolderHelpText": "Folder in which {appName} will store the {extension} file", "Book": "Book", "BookSearch": "Book Search", "BookSearchTypes": "Book Search Types", "Branch": "Branch", "BranchUpdate": "Branch to use to update {appName}", "BranchUpdateMechanism": "Branch used by external update mechanism", + "BuiltIn": "Built-In", "BypassProxyForLocalAddresses": "Bypass Proxy for Local Addresses", "Cancel": "Cancel", "CancelPendingTask": "Are you sure you want to cancel this pending task?", @@ -112,6 +123,7 @@ "Clear": "Clear", "ClearHistory": "Clear History", "ClearHistoryMessageText": "Are you sure you want to clear all {appName} history?", + "ClickToChangeQueryOptions": "Click to change query options", "ClientPriority": "Client Priority", "Clone": "Clone", "CloneProfile": "Clone Profile", @@ -131,12 +143,16 @@ "CountDownloadClientsSelected": "{count} download client(s) selected", "CountIndexersAvailable": "{count} indexer(s) available", "CountIndexersSelected": "{count} indexer(s) selected", + "CurrentlyInstalled": "Currently Installed", "Custom": "Custom", + "CustomFilter": "Custom Filter", "CustomFilters": "Custom Filters", - "DBMigration": "DB Migration", "Database": "Database", + "DatabaseMigration": "Database Migration", "Date": "Date", "Dates": "Dates", + "Default": "Default", + "DefaultCategory": "Default Category", "DefaultNameCopiedProfile": "{name} - Copy", "Delete": "Delete", "DeleteAppProfile": "Delete App Profile", @@ -162,21 +178,72 @@ "DeleteTag": "Delete Tag", "DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?", "Description": "Description", + "Destination": "Destination", "Details": "Details", "DevelopmentSettings": "Development Settings", + "Directory": "Directory", "Disabled": "Disabled", "DisabledForLocalAddresses": "Disabled for Local Addresses", "DisabledUntil": "Disabled Until", "Discord": "Discord", "Docker": "Docker", + "DockerUpdater": "Update the docker container to receive the update", + "Donate": "Donate", "Donations": "Donations", + "Download": "Download", "DownloadClient": "Download Client", + "DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location", "DownloadClientCategory": "Download Client Category", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Optional shared folder to put downloads into, leave blank to use the default Download Station location", + "DownloadClientFloodSettingsAdditionalTags": "Additional Tags", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adds properties of media as tags. Hints are examples.", + "DownloadClientFloodSettingsTagsHelpText": "Initial tags of a download. To be recognized, a download must have all initial tags. This avoids conflicts with unrelated downloads.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Adds a prefix to the Flood API, such as {url}", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Define Freebox API base URL with API version, eg '{url}', defaults to '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "App ID", + "DownloadClientFreeboxSettingsAppIdHelpText": "App ID given when creating access to Freebox API (ie 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "App Token", + "DownloadClientFreeboxSettingsAppTokenHelpText": "App token retrieved when creating access to Freebox API (ie 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Hostname or host IP address of the Freebox, defaults to '{url}' (will only work if on same network)", + "DownloadClientFreeboxSettingsPortHelpText": "Port used to access Freebox interface, defaults to '{port}'", + "DownloadClientNzbgetSettingsAddPausedHelpText": "This option requires at least NzbGet version 16.0", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb Folder", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "This folder will need to be reachable from XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Strm Folder", + "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone", "DownloadClientPriorityHelpText": "Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority.", + "DownloadClientQbittorrentSettingsContentLayout": "Content Layout", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Download first and last pieces first (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Initial state for torrents added to qBittorrent. Note that Forced Torrents do not abide by seed restrictions", + "DownloadClientQbittorrentSettingsSequentialOrder": "Sequential Order", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Download in sequential order (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.", + "DownloadClientRTorrentSettingsAddStopped": "Add Stopped", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Enabling will add torrents and magnets to rTorrent in a stopped state. This may break magnet files.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default rTorrent location", + "DownloadClientRTorrentSettingsUrlPath": "Url Path", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Path to the XMLRPC endpoint, see {url}. This is usually RPC2 or [path to ruTorrent]{url2} when using ruTorrent.", "DownloadClientSettings": "Download Client Settings", - "DownloadClientStatusCheckAllClientMessage": "All download clients are unavailable due to failures", - "DownloadClientStatusCheckSingleClientMessage": "Download clients unavailable due to failures: {0}", + "DownloadClientSettingsAddPaused": "Add Paused", + "DownloadClientSettingsDefaultCategoryHelpText": "Default fallback category if no mapped category exists for a release. Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended.", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Default fallback category if no mapped category exists for a release. Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended. Creates a [category] subdirectory in the output directory.", + "DownloadClientSettingsDestinationHelpText": "Manually specifies download destination, leave blank to use the default", + "DownloadClientSettingsInitialState": "Initial State", + "DownloadClientSettingsInitialStateHelpText": "Initial state for torrents added to {clientName}", + "DownloadClientSettingsPriorityItemHelpText": "Priority to use when grabbing items", + "DownloadClientSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} url, such as {url}", + "DownloadClientSettingsUseSslHelpText": "Use secure connection when connection to {clientName}", + "DownloadClientStatusAllClientHealthCheckMessage": "All download clients are unavailable due to failures", + "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", + "DownloadClientUTorrentProviderMessage": "uTorrent has a history of including cryptominers, malware and ads, we strongly encourage you to choose a different client.", "DownloadClients": "Download Clients", + "DownloadClientsLoadError": "Unable to load download clients", "DownloadClientsSettingsSummary": "Download clients configuration for integration into {appName} UI search", "Duration": "Duration", "Edit": "Edit", @@ -198,7 +265,7 @@ "EnableInteractiveSearch": "Enable Interactive Search", "EnableInteractiveSearchHelpText": "Will be used when interactive search is used", "EnableRss": "Enable RSS", - "EnableRssHelpText": "Enable Rss feed for Indexer", + "EnableRssHelpText": "Enable RSS feed for Indexer", "EnableSSL": "Enable SSL", "EnableSslHelpText": " Requires restart running as administrator to take effect", "Enabled": "Enabled", @@ -208,12 +275,16 @@ "Episode": "Episode", "Error": "Error", "ErrorLoadingContents": "Error loading contents", + "ErrorRestoringBackup": "Error restoring backup", "EventType": "Event Type", "Events": "Events", "Exception": "Exception", "ExistingTag": "Existing tag", "External": "External", + "ExternalUpdater": "{appName} is configured to use an external update mechanism", "Failed": "Failed", + "FailedToFetchSettings": "Failed to fetch settings", + "FailedToFetchUpdates": "Failed to fetch updates", "FeatureRequests": "Feature Requests", "Filename": "Filename", "Files": "Files", @@ -231,12 +302,13 @@ "GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates", "Genre": "Genre", "GoToApplication": "Go to application", + "GrabRelease": "Grab Release", "GrabReleases": "Grab Release(s)", "GrabTitle": "Grab Title", "Grabbed": "Grabbed", "Grabs": "Grabs", "Health": "Health", - "HealthNoIssues": "No issues with your configuration", + "HealthMessagesInfoBox": "You can find more information about the cause of these health check messages by clicking the wiki link (book icon) at the end of the row, or by checking your [logs]({link}). If you have difficulty interpreting these messages then you can reach out to our support, at the links below.", "HideAdvanced": "Hide Advanced", "History": "History", "HistoryCleanup": "History Cleanup", @@ -246,50 +318,141 @@ "HomePage": "Home Page", "Host": "Host", "Hostname": "Hostname", - "Id": "Id", + "Id": "ID", "IgnoredAddresses": "Ignored Addresses", "IllRestartLater": "I'll restart later", "Implementation": "Implementation", "IncludeHealthWarningsHelpText": "Include Health Warnings", "IncludeManualGrabsHelpText": "Include Manual Grabs made within {appName}", "Indexer": "Indexer", + "IndexerAlphaRatioSettingsExcludeScene": "Exclude SCENE", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Exclude SCENE releases from results", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Search freeleech releases only", "IndexerAlreadySetup": "At least one instance of indexer is already setup", "IndexerAuth": "Indexer Auth", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerAvistazSettingsPasswordHelpText": "Site Password", + "IndexerAvistazSettingsPidHelpText": "PID from My Account or My Profile page", + "IndexerAvistazSettingsUsernameHelpText": "Site Username", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Only member rank and above can use the API on this indexer.", + "IndexerBeyondHDSettingsApiKeyHelpText": "API Key from the Site (Found in My Security => API Key)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerBeyondHDSettingsLimitedOnly": "Limited Only", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Search freeleech only (Limited UL)", + "IndexerBeyondHDSettingsRefundOnly": "Refund Only", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only", + "IndexerBeyondHDSettingsRewindOnly": "Rewind Only", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Search rewind only", + "IndexerBeyondHDSettingsRssKeyHelpText": "RSS Key from the Site (Found in My Security => RSS Key)", + "IndexerBeyondHDSettingsSearchTypes": "Search Types", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Select the types of releases that you are interested in. If none selected, all options are used.", "IndexerCategories": "Indexer Categories", "IndexerDetails": "Indexer Details", "IndexerDisabled": "Indexer Disabled", - "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {0}.", + "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.", "IndexerDownloadClientHelpText": "Specify which download client is used for grabs made within {appName} from this indexer", "IndexerFailureRate": "Indexer Failure Rate", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerFileListSettingsPasskeyHelpText": "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", + "IndexerFileListSettingsUsernameHelpText": "Site Username", "IndexerFlags": "Indexer Flags", + "IndexerGazelleGamesSettingsApiKeyHelpText": "API Key from the Site (Found in Settings => Access Settings)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Must have User and Torrents permissions", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerGazelleGamesSettingsSearchGroupNames": "Search Group Names", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Search releases by group names", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerHDBitsSettingsCodecsHelpText": "If unspecified, all options are used.", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Show freeleech releases only", + "IndexerHDBitsSettingsMediums": "Mediums", + "IndexerHDBitsSettingsMediumsHelpText": "If unspecified, all options are used.", + "IndexerHDBitsSettingsOrigins": "Origins", + "IndexerHDBitsSettingsOriginsHelpText": "If unspecified, all options are used.", + "IndexerHDBitsSettingsPasskeyHelpText": "Passkey from User Details", + "IndexerHDBitsSettingsUseFilenames": "Use Filenames", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Check this option if you want to use torrent filenames as release titles", + "IndexerHDBitsSettingsUsernameHelpText": "Site Username", "IndexerHealthCheckNoIndexers": "No indexers enabled, {appName} will not return search results", "IndexerHistoryLoadError": "Error loading indexer history", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent associated with cookie used from Browser", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerId": "Indexer ID", "IndexerInfo": "Indexer Info", - "IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures for more than 6 hours", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures for more than 6 hours: {indexerNames}", + "IndexerMTeamTpSettingsApiKeyHelpText": "API Key from the Site (Found in User Control Panel => Security => Laboratory)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Search freeleech releases only", "IndexerName": "Indexer Name", - "IndexerNoDefCheckMessage": "Indexers have no definition and will not work: {0}. Please remove and (or) re-add to {appName}", + "IndexerNebulanceSettingsApiKeyHelpText": "API Key from User Settings > Api Keys. Key must have List and Download permissions", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Additional Newznab parameters", + "IndexerNewznabSettingsApiKeyHelpText": "Site API Key", + "IndexerNewznabSettingsVipExpirationHelpText": "Enter date (yyyy-mm-dd) for VIP Expiration or blank, {appName} will notify 1 week from expiration of VIP", + "IndexerNoDefinitionCheckHealthCheckMessage": "Indexers have no definition and will not work: {indexerNames}. Please remove and (or) re-add to {appName}.", + "IndexerNzbIndexSettingsApiKeyHelpText": "Site API Key", "IndexerObsoleteCheckMessage": "Indexers are obsolete or have been updated: {0}. Please remove and (or) re-add to {appName}", + "IndexerOrpheusSettingsApiKeyHelpText": "API Key from the Site (Found in Settings => Access Settings)", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Site API Key", + "IndexerPassThePopcornSettingsApiUserHelpText": "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Search freeleech releases only", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Golden Popcorn only", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Search Golden Popcorn releases only", "IndexerPriority": "Indexer Priority", "IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25.", "IndexerProxies": "Indexer Proxies", "IndexerProxy": "Indexer Proxy", - "IndexerProxyStatusCheckAllClientMessage": "All proxies are unavailable due to failures", - "IndexerProxyStatusCheckSingleClientMessage": "Proxies unavailable due to failures: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "All indexer proxies are unavailable due to failures", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Indexer proxies unavailable due to failures: {indexerProxyNames}", "IndexerQuery": "Indexer Query", - "IndexerRss": "Indexer Rss", + "IndexerRedactedSettingsApiKeyHelpText": "API Key from the Site (Found in Settings => Access Settings)", + "IndexerRss": "Indexer RSS", + "IndexerSettingsAdditionalParameters": "Additional Parameters", + "IndexerSettingsApiPath": "API Path", + "IndexerSettingsApiPathHelpText": "Path to the api, usually {url}", + "IndexerSettingsApiUser": "API User", + "IndexerSettingsAppsMinimumSeeders": "Apps Minimum Seeders", + "IndexerSettingsAppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default", + "IndexerSettingsBaseUrl": "Base Url", + "IndexerSettingsBaseUrlHelpText": "Select which base url {appName} will use for requests to the site", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "Site Cookie", + "IndexerSettingsFreeleechOnly": "Freeleech Only", + "IndexerSettingsGrabLimit": "Grab Limit", + "IndexerSettingsGrabLimitHelpText": "The number of max grabs as specified by the respective unit that {appName} will allow to the site", + "IndexerSettingsLimitsUnit": "Limits Unit", + "IndexerSettingsLimitsUnitHelpText": "The unit of time for counting limits per indexer", + "IndexerSettingsPackSeedTime": "Pack Seed Time", + "IndexerSettingsPackSeedTimeIndexerHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", + "IndexerSettingsPasskey": "Pass Key", + "IndexerSettingsPreferMagnetUrl": "Prefer Magnet URL", + "IndexerSettingsPreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links", + "IndexerSettingsQueryLimit": "Query Limit", + "IndexerSettingsQueryLimitHelpText": "The number of max queries as specified by the respective unit that {appName} will allow to the site", + "IndexerSettingsRssKey": "RSS Key", + "IndexerSettingsSeedRatio": "Seed Ratio", + "IndexerSettingsSeedRatioHelpText": "The ratio a torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules", + "IndexerSettingsSeedTime": "Seed Time", + "IndexerSettingsSeedTimeHelpText": "The time a torrent should be seeded before stopping, empty uses the download client's default", "IndexerSettingsSummary": "Configure various global Indexer settings including Proxies.", + "IndexerSettingsVipExpiration": "VIP Expiration", "IndexerSite": "Indexer Site", "IndexerStatus": "Indexer Status", - "IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures", - "IndexerStatusCheckSingleClientMessage": "Indexers unavailable due to failures: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures", + "IndexerStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures: {indexerNames}", "IndexerTagsHelpText": "Use tags to specify Indexer Proxies or which apps the indexer is synced to.", "IndexerTagsHelpTextWarning": "Tags should be used with caution, they can have unintended effects. An indexer with a tag will only sync to apps with the same tag.", - "IndexerVipCheckExpiredClientMessage": "Indexer VIP benefits have expired: {0}", - "IndexerVipCheckExpiringClientMessage": "Indexer VIP benefits expiring soon: {0}", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Site API Key", + "IndexerVipExpiredHealthCheckMessage": "Indexer VIP benefits have expired: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Indexer VIP benefits expiring soon: {indexerNames}", "Indexers": "Indexers", "Info": "Info", + "InfoUrl": "Info URL", "InitialFailure": "Initial Failure", + "Install": "Install", + "InstallLatest": "Install Latest", + "InstallMajorVersionUpdate": "Install Update", + "InstallMajorVersionUpdateMessage": "This update will install a new major version and may not be compatible with your system. Are you sure you want to install this update?", + "InstallMajorVersionUpdateMessageLink": "Please check [{domain}]({url}) for more information.", "InstanceName": "Instance Name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InteractiveSearch": "Interactive Search", @@ -297,6 +460,7 @@ "InvalidUILanguage": "Your UI is set to an invalid language, correct it and save your settings", "KeyboardShortcuts": "Keyboard Shortcuts", "Label": "Label", + "LabelIsRequired": "Label is required", "Language": "Language", "LastDuration": "Last Duration", "LastExecution": "Last Execution", @@ -306,22 +470,30 @@ "Level": "Level", "Link": "Link", "LogFiles": "Log Files", + "LogFilesLocation": "Log files are located in: {location}", "LogLevel": "Log Level", "LogLevelTraceHelpTextWarning": "Trace logging should only be enabled temporarily", + "LogSizeLimit": "Log Size Limit", + "LogSizeLimitHelpText": "Maximum log file size in MB before archiving. Default is 1MB.", "Logging": "Logging", + "Logout": "Logout", "Logs": "Logs", "MIA": "MIA", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "ManageApplications": "Manage Applications", + "ManageClients": "Manage Clients", "ManageDownloadClients": "Manage Download Clients", "Manual": "Manual", + "ManualGrab": "Manual Grab", "MappedCategories": "Mapped Categories", "MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information", "MassEditor": "Mass Editor", "Mechanism": "Mechanism", + "Menu": "Menu", "Message": "Message", "MinimumSeeders": "Minimum Seeders", "MinimumSeedersHelpText": "Minimum seeders required by the Application for the indexer to grab", + "Mixed": "Mixed", "Mode": "Mode", "More": "More", "MoreInfo": "More Info", @@ -336,14 +508,17 @@ "NewznabUrl": "Newznab Url", "NextExecution": "Next Execution", "No": "No", + "NoApplicationsFound": "No applications found", "NoBackupsAreAvailable": "No backups are available", "NoChange": "No Change", "NoChanges": "No Changes", "NoDownloadClientsFound": "No download clients found", + "NoEventsFound": "No events found", "NoHistoryFound": "No history found", "NoIndexerCategories": "No categories found for this indexer", "NoIndexerHistory": "No history found for this indexer", "NoIndexersFound": "No indexers found", + "NoIssuesWithYourConfiguration": "No issues with your configuration", "NoLeaveIt": "No, Leave It", "NoLinks": "No Links", "NoLogFiles": "No log files", @@ -354,10 +529,14 @@ "NotSupported": "Not Supported", "Notification": "Notification", "NotificationStatusAllClientHealthCheckMessage": "All notifications are unavailable due to failures", - "NotificationStatusSingleClientHealthCheckMessage": "Notifications unavailable due to failures: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notifications unavailable due to failures: {notificationNames}", "NotificationTriggers": "Notification Triggers", "NotificationTriggersHelpText": "Select which events should trigger this notification", "Notifications": "Notifications", + "NotificationsEmailSettingsUseEncryption": "Use Encryption", + "NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption", + "NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications", "OAuthPopupMessage": "Pop-ups are being blocked by your browser", "Ok": "Ok", "OnApplicationUpdate": "On Application Update", @@ -368,12 +547,17 @@ "OnHealthIssueHelpText": "On Health Issue", "OnHealthRestored": "On Health Restored", "OnHealthRestoredHelpText": "On Health Restored", + "OnLatestVersion": "The latest version of {appName} is already installed", + "Open": "Open", "OpenBrowserOnStart": "Open browser on start", "OpenThisModal": "Open This Modal", "Options": "Options", + "OverrideAndAddToDownloadClient": "Override and add to download client", + "OverrideGrabModalTitle": "Override and Grab - {title}", "PackSeedTime": "Pack Seed Time", "PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", "PackageVersion": "Package Version", + "PackageVersionInfo": "{packageVersion} by {packageAuthor}", "PageSize": "Page Size", "PageSizeHelpText": "Number of items to show on each page", "Parameters": "Parameters", @@ -385,23 +569,32 @@ "PendingChangesStayReview": "Stay and review changes", "Port": "Port", "PortNumber": "Port Number", + "PreferMagnetUrl": "Prefer Magnet URL", + "PreferMagnetUrlHelpText": "When enabled, this indexer will prefer the use of magnet URLs for grabs with fallback to torrent links", "Presets": "Presets", + "PreviouslyInstalled": "Previously Installed", "Priority": "Priority", + "PrioritySettings": "Priority: {priority}", "Privacy": "Privacy", "Private": "Private", "Protocol": "Protocol", + "ProwlarrDownloadClientsAlert": "If you intend to do searches directly within {appName}, you need to add Download Clients. Otherwise, you do not need to add them here. For searches from your Apps, the download clients configured there are used instead.", + "ProwlarrDownloadClientsInAppOnlyAlert": "Download clients are for {appName} in-app searches only and do not sync to apps. There are no plans to add any such functionality.", "ProwlarrSupportsAnyDownloadClient": "{appName} supports any of the download clients listed below.", "ProwlarrSupportsAnyIndexer": "{appName} supports many indexers in addition to any indexer that uses the Newznab/Torznab standard using 'Generic Newznab' (for usenet) or 'Generic Torznab' (for torrents). Search & Select your indexer from below.", "Proxies": "Proxies", "Proxy": "Proxy", + "ProxyBadRequestHealthCheckMessage": "Failed to test proxy. Status code: {statusCode}", "ProxyBypassFilterHelpText": "Use ',' as a separator, and '*.' as a wildcard for subdomains", - "ProxyCheckBadRequestMessage": "Failed to test proxy. Status code: {0}", - "ProxyCheckFailedToTestMessage": "Failed to test proxy: {0}", - "ProxyCheckResolveIpMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}", + "ProxyFailedToTestHealthCheckMessage": "Failed to test proxy: {url}", "ProxyPasswordHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.", + "ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {proxyHostName}", "ProxyType": "Proxy Type", "ProxyUsernameHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.", + "ProxyValidationBadRequest": "Failed to test proxy. Status code: {statusCode}", + "ProxyValidationUnableToConnect": "Unable to connect to proxy: {exceptionMessage}. Check the log surrounding this error for details", "Public": "Public", + "PublishedDate": "Published Date", "Publisher": "Publisher", "Query": "Query", "QueryOptions": "Query Options", @@ -409,14 +602,13 @@ "QueryType": "Query Type", "Queue": "Queue", "Queued": "Queued", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS is not supported with this indexer", "RawSearchSupported": "Raw Search Supported", "ReadTheWikiForMoreInformation": "Read the Wiki for more information", "RecentChanges": "Recent Changes", "Reddit": "Reddit", "Redirect": "Redirect", "RedirectHelpText": "Redirect incoming download request for indexer and pass the grab directly instead of proxying the request via {appName}", + "Redirected": "Redirected", "Refresh": "Refresh", "RefreshMovie": "Refresh movie", "ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid {appName} release branch, you will not receive updates", @@ -434,12 +626,15 @@ "Restart": "Restart", "RestartNow": "Restart Now", "RestartProwlarr": "Restart {appName}", + "RestartReloadNote": "Note: {appName} will automatically restart and reload the UI during the restore process.", "RestartRequiredHelpTextWarning": "Requires restart to take effect", "Restore": "Restore", "RestoreBackup": "Restore Backup", "Result": "Result", "Retention": "Retention", + "Rss": "RSS", "RssFeed": "RSS Feed", + "RssIsNotSupportedWithThisIndexer": "RSS is not supported with this indexer", "RssQueries": "RSS Queries", "SSLCertPassword": "SSL Cert Password", "SSLCertPasswordHelpText": "Password for pfx file", @@ -450,6 +645,7 @@ "SaveChanges": "Save Changes", "SaveSettings": "Save Settings", "Scheduled": "Scheduled", + "Script": "Script", "ScriptPath": "Script Path", "Search": "Search", "SearchAllIndexers": "Search all indexers", @@ -460,6 +656,7 @@ "SearchType": "Search Type", "SearchTypes": "Search Types", "Season": "Season", + "SecretToken": "Secret Token", "Security": "Security", "SeedRatio": "Seed Ratio", "SeedRatioHelpText": "The ratio a torrent should reach before stopping, empty is app's default", @@ -467,6 +664,7 @@ "SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default", "Seeders": "Seeders", "SelectAll": "Select All", + "SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client", "SelectIndexers": "Select Indexers", "SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases", "SemiPrivate": "Semi-Private", @@ -498,7 +696,7 @@ "Source": "Source", "StartTypingOrSelectAPathBelow": "Start typing or select a path below", "Started": "Started", - "StartupDirectory": "Startup directory", + "StartupDirectory": "Startup Directory", "Stats": "Stats", "Status": "Status", "StopSelecting": "Stop Selecting", @@ -511,7 +709,7 @@ "SyncProfile": "Sync Profile", "SyncProfiles": "Sync Profiles", "System": "System", - "SystemTimeCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", + "SystemTimeHealthCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", "TVSearchTypes": "TV Search Types", "TableOptions": "Table Options", "TableOptionsColumnsMessage": "Choose which columns are visible and which order they appear in", @@ -526,7 +724,7 @@ "TestAllApps": "Test All Apps", "TestAllClients": "Test All Clients", "TestAllIndexers": "Test All Indexers", - "TheLatestVersionIsAlreadyInstalled": "The latest version of {appName} is already installed", + "TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in [General Settings](/settings/general)", "Theme": "Theme", "ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by {inspiredBy}.", "Time": "Time", @@ -534,6 +732,11 @@ "Today": "Today", "Tomorrow": "Tomorrow", "Torrent": "Torrent", + "TorrentBlackholeSaveMagnetFiles": "Save Magnet Files", + "TorrentBlackholeSaveMagnetFilesExtension": "Save Magnet Files Extension", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension to use for magnet links, defaults to '.magnet'", + "TorrentBlackholeSaveMagnetFilesHelpText": "Save the magnet link if no .torrent file is available (only useful if the download client supports magnets saved to a file)", + "TorrentBlackholeTorrentFolder": "Torrent Folder", "Torrents": "Torrents", "TorznabUrl": "Torznab Url", "TotalGrabs": "Total Grabs", @@ -561,10 +764,7 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "Unable to add a new indexer proxy, please try again.", "UnableToAddANewNotificationPleaseTryAgain": "Unable to add a new notification, please try again.", "UnableToLoadAppProfiles": "Unable to load app profiles", - "UnableToLoadApplicationList": "Unable to load application list", - "UnableToLoadBackups": "Unable to load backups", "UnableToLoadDevelopmentSettings": "Unable to load Development settings", - "UnableToLoadDownloadClients": "Unable to load download clients", "UnableToLoadGeneralSettings": "Unable to load General settings", "UnableToLoadHistory": "Unable to load history", "UnableToLoadIndexerProxies": "Unable to load Indexer Proxies", @@ -574,19 +774,23 @@ "UnableToLoadUISettings": "Unable to load UI settings", "UnsavedChanges": "Unsaved Changes", "UnselectAll": "Unselect All", + "UpdateAppDirectlyLoadError": "Unable to update {appName} directly,", "UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates", - "UpdateAvailable": "New update is available", - "UpdateCheckStartupNotWritableMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.", - "UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", + "UpdateAvailableHealthCheckMessage": "New update is available: {version}", "UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script", "UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process", + "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is not writable by the user '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.", + "UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.", + "UpdaterLogFiles": "Updater Log Files", "Updates": "Updates", "Uptime": "Uptime", "Url": "Url", "UrlBaseHelpText": "For reverse proxy support, default is empty", "UseProxy": "Use Proxy", + "UseSsl": "Use SSL", "Usenet": "Usenet", + "UsenetBlackholeNzbFolder": "Nzb Folder", "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent provided by the app that called the API", "Username": "Username", "Version": "Version", @@ -596,6 +800,8 @@ "Website": "Website", "WhatsNew": "What's New?", "Wiki": "Wiki", + "WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?", + "XmlRpcPath": "XML RPC Path", "Year": "Year", "Yes": "Yes", "YesCancel": "Yes, Cancel", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 709d36070..e7981a69e 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1,8 +1,8 @@ { "Indexers": "Indexadores", "Host": "Host", - "History": "Historia", - "HideAdvanced": "Ocultar Avanzado", + "History": "Historial", + "HideAdvanced": "Ocultar avanzado", "Health": "Salud", "General": "General", "Folder": "Carpeta", @@ -10,10 +10,10 @@ "Files": "Archivos", "Events": "Eventos", "Edit": "Editar", - "DownloadClientStatusCheckSingleClientMessage": "Gestores de descargas no disponibles debido a errores: {0}", - "DownloadClientStatusCheckAllClientMessage": "Los gestores de descargas no están disponibles debido a errores", - "DownloadClients": "Gestores de Descargas", - "Delete": "Borrar", + "DownloadClientStatusSingleClientHealthCheckMessage": "Gestores de descargas no disponibles debido a errores: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Los gestores de descargas no están disponibles debido a errores", + "DownloadClients": "Clientes de descarga", + "Delete": "Eliminar", "Dates": "Fechas", "Date": "Fecha", "CustomFilters": "Filtros Personalizados", @@ -22,74 +22,74 @@ "Clear": "Borrar", "BackupNow": "Hacer copia de seguridad ahora", "Backup": "Copia de seguridad", - "AppDataLocationHealthCheckMessage": "No será posible actualizar para prevenir la eliminación de AppData al Actualizar", + "AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar", "Analytics": "Analíticas", "All": "Todo", "About": "Acerca de", "View": "Vista", "Updates": "Actualizaciones", - "UpdateCheckUINotWritableMessage": "No se puede instalar la actualización porque la carpeta UI '{0}' no tiene permisos de escritura para el usuario '{1}'.", - "UpdateCheckStartupTranslocationMessage": "No se puede instalar la actualización porque la carpeta de arranque '{0}' está en una carpeta de \"App Translocation\".", - "UpdateCheckStartupNotWritableMessage": "No se puede instalar la actualización porque la carpeta de arranque '{0}' no tiene permisos de escritura para el usuario '{1}'.", - "UnselectAll": "Deseleccionar Todo", - "UI": "UI", + "UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz de usuario '{uiFolder}' no es modificable por el usuario '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de translocación de aplicaciones.", + "UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' no tiene permisos de escritura para el usuario '{userName}'.", + "UnselectAll": "Desmarcar todo", + "UI": "Interfaz", "Tasks": "Tareas", "Tags": "Etiquetas", "System": "Sistema", "Style": "Estilo", "Status": "Estado", "Sort": "Ordenar", - "ShowAdvanced": "Mostrar Avanzado", + "ShowAdvanced": "Mostrar avanzado", "Settings": "Ajustes", - "SetTags": "Poner Etiquetas", - "SelectAll": "Seleccionar Todas", + "SetTags": "Establecer etiquetas", + "SelectAll": "Seleccionar todo", "Security": "Seguridad", "Search": "Buscar", "Scheduled": "Programado", - "SaveChanges": "Guardar Cambios", - "RestoreBackup": "Recuperar Backup", - "ReleaseBranchCheckOfficialBranchMessage": "Las versión {0} no es una versión válida de {appName}, no recibirás actualizaciones", + "SaveChanges": "Guardar cambios", + "RestoreBackup": "Restaurar copia de seguridad", + "ReleaseBranchCheckOfficialBranchMessage": "La rama {0} no es una rama de lanzamiento válida de {appName}, no recibirás actualizaciones", "Refresh": "Actualizar", "Queue": "Cola", - "ProxyCheckResolveIpMessage": "No se pudo resolver la dirección IP del Host Proxy configurado {0}", - "ProxyCheckFailedToTestMessage": "Fallo al comprobar el proxy: {0}", - "ProxyCheckBadRequestMessage": "Fallo al comprobar el proxy. StatusCode: {0}", + "ProxyResolveIpHealthCheckMessage": "No se pudo resolver la dirección IP del Host Proxy configurado {proxyHostName}", + "ProxyFailedToTestHealthCheckMessage": "Fallo al comprobar el proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Fallo al comprobar el proxy. Status code: {statusCode}", "Proxy": "Proxy", "Options": "Opciones", - "NoChange": "Sin Cambio", - "NoChanges": "Sin Cambios", - "MoreInfo": "Más Información", + "NoChange": "Sin cambio", + "NoChanges": "Sin cambios", + "MoreInfo": "Más información", "Logging": "Registro de eventos", "LogFiles": "Archivos de Registro", "Language": "Idioma", - "IndexerStatusCheckAllClientMessage": "Los indexers no están disponibles debido a errores", + "IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores", "Added": "Añadido", "Actions": "Acciones", - "UISettingsSummary": "Calendario, fecha y opciones de color deteriorado", - "TagsSettingsSummary": "Ver todas las etiquetas y cómo se usan. Las etiquetas no utilizadas se pueden eliminar", + "UISettingsSummary": "Fecha, idioma, y opciones de color deteriorado", + "TagsSettingsSummary": "Vea todas las etiquetas y cómo se usan. Las etiquetas sin usar pueden ser eliminadas", "Size": "Tamaño", "ReleaseStatus": "Estado del Estreno", "Protocol": "Protocolo", "LastWriteTime": "Última Fecha de Escritura", - "IndexerStatusCheckSingleClientMessage": "Indexers no disponibles debido a errores: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}", "Indexer": "Indexador", - "Grabbed": "Añadido", - "GeneralSettingsSummary": "Puerto, SSL, nombre de usuario/contraseña , proxy, analíticas y actualizaciones", - "Filename": "Nombre del archivo", + "Grabbed": "Capturado", + "GeneralSettingsSummary": "Puerto, SSL, nombre de usuario/contraseña , proxy, analíticas, y actualizaciones", + "Filename": "Nombre de archivo", "Failed": "Fallido", - "EventType": "Tipo de Evento", - "DownloadClientsSettingsSummary": "Gestores de descargas, manipulación de descargas y mapeados remotos", - "DownloadClient": "Gestor de Descargas", + "EventType": "Tipo de evento", + "DownloadClientsSettingsSummary": "Configuración del cliente de descargas para la integración en {appName} UI search", + "DownloadClient": "Cliente de descarga", "Details": "Detalles", - "ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados", + "ConnectSettingsSummary": "Notificaciones y scripts personalizados", "Warn": "Advertencia", "Type": "Tipo", "Title": "Título", "Time": "Tiempo", - "TestAll": "Testear Todo", - "Test": "Test", + "TestAll": "Probar todo", + "Test": "Prueba", "TableOptionsColumnsMessage": "Escoger qué columnas son visibles y en que orden aparecerán", - "TableOptions": "Opciones de Tabla", + "TableOptions": "Opciones de tabla", "Source": "Fuente", "Shutdown": "Apagar", "Seeders": "Semillas", @@ -99,13 +99,13 @@ "Peers": "Pares", "PageSize": "Tamaño de Página", "Ok": "Ok", - "OAuthPopupMessage": "Pop-ups bloqueados por su navegador", + "OAuthPopupMessage": "Los elementos emergentes están siendo bloqueados por tu navegador", "Name": "Nombre", "Message": "Mensaje", "Level": "Nivel", "KeyboardShortcuts": "Atajos de Teclado", "Info": "Info", - "HealthNoIssues": "No hay problemas con tu configuración", + "NoIssuesWithYourConfiguration": "No hay problemas con tu configuración", "Error": "Error", "ConnectionLost": "Conexión perdida", "Component": "Componente", @@ -114,36 +114,36 @@ "Cancel": "Cancelar", "Apply": "Aplicar", "Age": "Antigüedad", - "SystemTimeCheckMessage": "El reloj del sistema está retrasado más de un día. Las tareas de mantenimiento no se ejecutarán correctamente hasta que se haya corregido", - "UnsavedChanges": "Cambios no guardados", - "ShowSearchHelpText": "Mostrar botón de búsqueda al pasar el cursor por encima", - "ShowSearch": "Mostrar Búsqueda", + "SystemTimeHealthCheckMessage": "El reloj del sistema está retrasado más de un día. Las tareas de mantenimiento no se ejecutarán correctamente hasta que se haya corregido", + "UnsavedChanges": "Cambios sin guardar", + "ShowSearchHelpText": "Muestra el botón de búsqueda al pasar por encima", + "ShowSearch": "Mostrar búsqueda", "SettingsTimeFormat": "Formato de Hora", "SettingsShowRelativeDatesHelpText": "Mostrar fechas relativas (Hoy/Ayer/etc) o absolutas", "SettingsShowRelativeDates": "Mostrar Fechas Relativas", "SettingsShortDateFormat": "Formato Corto de Fecha", "SettingsLongDateFormat": "Formato Largo de Fecha", - "PendingChangesStayReview": "Permanecer y revisar cambios", - "PendingChangesMessage": "Hay cambios sin salvar, estás seguro de que quieres salir de esta página?", + "PendingChangesStayReview": "Quedarse y revisar cambios", + "PendingChangesMessage": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir de esta página?", "PendingChangesDiscardChanges": "Descartar cambios y salir", "SettingsEnableColorImpairedModeHelpText": "Estilo modificado para permitir que usuarios con problemas de color distingan mejor la información codificada por colores", "SettingsEnableColorImpairedMode": "Activar Modo De Color Degradado", - "GeneralSettings": "Ajustes Generales", + "GeneralSettings": "Opciones generales", "Fixed": "Arreglado", "EnableSslHelpText": " Requiere reiniciar la aplicación como administrador para que surta efecto", "Enable": "Habilitar", - "DownloadClientSettings": "Ajustes de Gestor de Descargas", + "DownloadClientSettings": "Opciones del cliente de descarga", "Docker": "Docker", - "DeleteTag": "Borrar Etiqueta", - "DeleteNotification": "Borrar Notificación", - "DeleteDownloadClient": "Borrar Gestor de Descargas", - "DeleteBackup": "Borrar Backup", - "DBMigration": "Migración de DB", + "DeleteTag": "Eliminar Etiqueta", + "DeleteNotification": "Eliminar Notificación", + "DeleteDownloadClient": "Borrar cliente de descarga", + "DeleteBackup": "Eliminar copia de seguridad", + "DatabaseMigration": "Migración de la base de datos", "CloneProfile": "Clonar Perfil", - "ClientPriority": "Prioridad de Cliente", + "ClientPriority": "Prioridad del Cliente", "ChangeHasNotBeenSavedYet": "El cambio aún no se ha guardado", "CertificateValidationHelpText": "Cambiar la rigidez de la validación de la certificación HTTPS", - "CertificateValidation": "Validación del certificado", + "CertificateValidation": "Validación de certificado", "BypassProxyForLocalAddresses": "Omitir Proxy para Direcciones Locales", "Branch": "Rama", "BindAddressHelpText": "Dirección IP4 válida, localhost o '*' para todas las interfaces", @@ -160,51 +160,51 @@ "AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", "YesCancel": "Sí, Cancelar", "Version": "Versión", - "Username": "Nombre de usuario", - "UseProxy": "Usar el Proxy", + "Username": "Usuario", + "UseProxy": "Usar proxy", "Usenet": "Usenet", - "UrlBaseHelpText": "Para soporte de reverse proxy, vacio por defecto", - "URLBase": "URL Base", + "UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío", + "URLBase": "URL base", "Uptime": "Tiempo de actividad", - "UpdateScriptPathHelpText": "Ruta del script propio que toma el paquete de actualización y se encarga del proceso de actualización restante", - "UpdateMechanismHelpText": "Usar el actualizador de {appName} o un script", - "UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Se podrán instalar desde Sistema: Actualizaciones también", + "UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización", + "UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script", + "UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones", "UnableToLoadTags": "No se pueden cargar las Etiquetas", "UnableToLoadNotifications": "No se pueden cargar las Notificaciones", - "UnableToLoadDownloadClients": "No se puden cargar los gestores de descargas", + "DownloadClientsLoadError": "No se pudieron cargar los clientes de descargas", "UISettings": "Ajustes del UI", "Torrents": "Torrents", - "TestAllClients": "Comprobar Todos los Gestores", + "TestAllClients": "Probar todos los clientes", "TagsHelpText": "Se aplica a películas con al menos una etiqueta coincidente", "SuggestTranslationChange": "Sugerir un cambio en la traducción", - "StartupDirectory": "Directorio de arranque", + "StartupDirectory": "Directorio de Arranque", "SSLPort": "Puerto SSL", "SSLCertPath": "Ruta del Certificado SSL", "SSLCertPassword": "Contraseña del Certificado SSL", - "SendAnonymousUsageData": "Enviar Datos de Uso Anónimamente", - "ScriptPath": "Ruta del Script", + "SendAnonymousUsageData": "Enviar datos de uso anónimos", + "ScriptPath": "Ruta del script", "Retention": "Retención", "Result": "Resultado", - "RestartRequiredHelpTextWarning": "Requiere reiniciar para que surta efecto", + "RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto", "RestartProwlarr": "Reiniciar {appName}", - "RestartNow": "Reiniciar Ahora", - "ResetAPIKey": "Reajustar API", + "RestartNow": "Reiniciar ahora", + "ResetAPIKey": "Restablecer clave API", "Reset": "Reiniciar", "RemoveFilter": "Eliminar filtro", "RemovedFromTaskQueue": "Eliminar de la cola de tareas", "RefreshMovie": "Actualizar película", "ReadTheWikiForMoreInformation": "Lee la Wiki para más información", - "ProxyUsernameHelpText": "Tienes que introducir tu nombre de usuario y contraseña sólo si son requeridos. Si no, déjalos vacios.", - "ProxyType": "Tipo de Proxy", - "ProxyPasswordHelpText": "Tienes que introducir tu nombre de usuario y contraseña sólo si son requeridos. Si no, déjalos vacios.", - "ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como wildcard para subdominios", - "PortNumber": "Número de Puerto", + "ProxyUsernameHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "ProxyType": "Tipo de proxy", + "ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios", + "PortNumber": "Número de puerto", "Port": "Puerto", "Password": "Contraseña", "PageSizeHelpText": "Número de elementos por página", "PackageVersion": "Versión del paquete", - "NotificationTriggers": "Desencadenantes de Notificaciones", - "NoLeaveIt": "No, Déjalo", + "NotificationTriggers": "Disparadores de notificación", + "NoLeaveIt": "No, déjalo", "New": "Nuevo", "NetCore": ".NET Core", "Mode": "Modo", @@ -212,119 +212,119 @@ "Mechanism": "Mecanismo", "Logs": "Registros", "LogLevel": "Nivel de Registro", - "LaunchBrowserHelpText": " Abrir un navegador web e ir a la página de inicio de {appName} al arrancar la app.", + "LaunchBrowserHelpText": " Abre un navegador web y navega a la página de inicio de {appName} al iniciarse la aplicación.", "Interval": "Intervalo", - "IndexerFlags": "Marcas de Indexer", + "IndexerFlags": "Indicadores del indexador", "IncludeHealthWarningsHelpText": "Incluir Alertas de Salud", "IllRestartLater": "Lo reiniciaré más tarde", - "IgnoredAddresses": "Direcciones Ignoradas", - "Hostname": "Nombre del Host", + "IgnoredAddresses": "Ignorar direcciones", + "Hostname": "Nombre de host", "EnableSSL": "Habilitar SSL", "EnableInteractiveSearch": "Habilitar Búsqueda Interactiva", "EnableAutomaticSearch": "Habilitar Búsqueda Automática", "ConnectSettings": "Conectar Ajustes", "BindAddress": "Dirección de Ligado", - "OpenBrowserOnStart": "Abrir navegador al arrancar", + "OpenBrowserOnStart": "Abrir navegador al inicio", "OnHealthIssueHelpText": "En Problema de Salud", "TagCannotBeDeletedWhileInUse": "No se puede eliminar estando en uso", "SSLCertPathHelpText": "Ruta al archivo pfx", "SSLCertPasswordHelpText": "Contraseña para el archivo pfx", - "RSSIsNotSupportedWithThisIndexer": "RSS no son soportadas por este indexer", + "RssIsNotSupportedWithThisIndexer": "RSS no está soportado con este indexador", "RemovingTag": "Eliminando etiqueta", "Manual": "Manual", - "LogLevelTraceHelpTextWarning": "El registro de seguimiento se ha de habilitar solo temporalmente", - "ExistingTag": "Etiqueta existente", + "LogLevelTraceHelpTextWarning": "El registro de seguimiento sólo debe activarse temporalmente", + "ExistingTag": "Etiquetas existentes", "EnableInteractiveSearchHelpText": "Se usará cuando se utilice la búsqueda interactiva", - "EnableAutomaticSearchHelpText": "Se usará cuando las búsquedas automáticas se realicen desde el UI o por {appName}", - "DeleteTagMessageText": "Seguro que quieres eliminar la etiqueta '{0}'?", - "DeleteNotificationMessageText": "Seguro que quieres elminiar la notificación '{0}'?", - "DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{0}'?", - "DeleteDownloadClientMessageText": "Seguro que quieres eliminar el gestor de descargas '{0}'?", - "CancelPendingTask": "Seguro que quieres cancelar esta tarea pendiente?", - "BranchUpdateMechanism": "Rama usada por el mecanismo de actualización externo", - "BranchUpdate": "Qué rama usar para actualizar {appName}", + "EnableAutomaticSearchHelpText": "Será usado cuando las búsquedas automáticas sean realizadas por la interfaz de usuario o por {appName}", + "DeleteTagMessageText": "¿Estás seguro que quieres eliminar la etiqueta '{label}'?", + "DeleteNotificationMessageText": "¿Estás seguro que quieres eliminar la notificación '{name}'?", + "DeleteBackupMessageText": "¿Estás seguro que quieres eliminar la copia de seguridad '{name}'?", + "DeleteDownloadClientMessageText": "¿Estás seguro que quieres eliminar el cliente de descarga '{name}'?", + "CancelPendingTask": "¿Estás seguro que quieres cancelar esta tarea pendiente?", + "BranchUpdateMechanism": "Rama usada por un mecanismo de actualización externo", + "BranchUpdate": "Rama a usar para actualizar {appName}", "BeforeUpdate": "Antes de actualizar", "AddingTag": "Añadir etiqueta", "UnableToLoadUISettings": "No se han podido cargar los ajustes de UI", "UnableToLoadHistory": "No se ha podido cargar la historia", "UnableToLoadGeneralSettings": "No se han podido cargar los ajustes Generales", - "UnableToLoadBackups": "No se han podido cargar las copias de seguridad", + "BackupsLoadError": "No se pudo cargar las copias de seguridad", "UnableToAddANewNotificationPleaseTryAgain": "No se ha podido añadir una nueva notificación, prueba otra vez.", - "UnableToAddANewIndexerPleaseTryAgain": "No se ha podido añadir un nuevo indexer, prueba otra vez.", + "UnableToAddANewIndexerPleaseTryAgain": "No se pudo añadir un nuevo indexador, por favor inténtalo de nuevo.", "UnableToAddANewDownloadClientPleaseTryAgain": "No se ha podido añadir un nuevo gestor de descargas, prueba otra vez.", "TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada", "StartTypingOrSelectAPathBelow": "Comienza a escribir o selecciona una ruta debajo", "Restore": "Restaurar", - "ProwlarrSupportsAnyIndexer": "{appName} soporta cualquier indexer que utilice el estandar Newznab, como también cualquiera de los indexers listados debajo.", - "ProwlarrSupportsAnyDownloadClient": "Raddar soporta cualquier gestor de descargas que utilice el estandar Newznab, como también los clientes indicados debajo.", + "ProwlarrSupportsAnyIndexer": "{appName} soporta muchos indexadores junto a cualquier indexador que utilice el estándar Newznab/Torznab usando 'Newznab genérico' (para usenet) o 'Torznab genérico' (para torrents). Busca y selecciona tu indexador a continuación.", + "ProwlarrSupportsAnyDownloadClient": "{appName} soporta cualquier gestor de descargas indicado debajo.", "NoUpdatesAreAvailable": "No hay actualizaciones disponibles", - "NoTagsHaveBeenAddedYet": "No se han añadido etiquetas todavía", - "NoLogFiles": "Sin archivos de registro", + "NoTagsHaveBeenAddedYet": "Ninguna etiqueta ha sido añadida aún", + "NoLogFiles": "No hay archivos de registro", "NoBackupsAreAvailable": "No hay copias de seguridad disponibles", - "MaintenanceRelease": "Lanzamiento de mantenimiento", + "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver el historial de commits de Github para más detalles", "ForMoreInformationOnTheIndividualDownloadClients": "Para más información individual de los gestores de descarga, haz clic en lls botones de información.", - "FilterPlaceHolder": "Buscar películas", + "FilterPlaceHolder": "Buscar Indexadores", "Exception": "Excepción", - "ErrorLoadingContents": "Error al cargar los contenidos", + "ErrorLoadingContents": "Error cargando contenidos", "UILanguageHelpTextWarning": "Recargar el Navegador", "UILanguageHelpText": "Lenguaje que {appName} usara para el UI", "UILanguage": "Lenguaje de UI", "Priority": "Prioridad", "InteractiveSearch": "Búsqueda Interactiva", - "IndexerPriorityHelpText": "Prioridad del Indexer de 1 (La más alta) a 50 (La más baja). Por defecto: 25.", - "IndexerPriority": "Prioridad del Indexer", - "EditIndexer": "Editar Indexer", + "IndexerPriorityHelpText": "Prioridad del indexador de 1 (la más alta) a 50 (la más baja). Predeterminado: 25.", + "IndexerPriority": "Prioridad del indexador", + "EditIndexer": "Editar indexador", "Disabled": "Deshabilitado", "AutomaticSearch": "Búsqueda Automática", "AddIndexer": "Añadir Indexador", - "FocusSearchBox": "Enfocar Cuadro de búsqueda", - "SaveSettings": "Grabar Ajustes", - "OpenThisModal": "Abrir este Modal", - "MovieIndexScrollTop": "Indice de Películas: Desplazar hacia arriba", - "MovieIndexScrollBottom": "Indice de Películas: Desplazar hacia abajo", - "CloseCurrentModal": "Cerrar Modal Actual", - "AcceptConfirmationModal": "Aceptar la confirmación modal", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexers no disponible por errores durando más de 6 horas: {0}", - "IndexerLongTermStatusCheckAllClientMessage": "Ningún indexer está disponible por errores durando más de 6 horas", + "FocusSearchBox": "Enfocar Campo de Búsqueda", + "SaveSettings": "Guardar ajustes", + "OpenThisModal": "Abrir esta Ventana Modal", + "MovieIndexScrollTop": "Índice de Películas: Desplazar hacia arriba", + "MovieIndexScrollBottom": "Índice de Películas: Desplazar hacia abajo", + "CloseCurrentModal": "Cerrar esta Ventana Modal", + "AcceptConfirmationModal": "Aceptar Confirmación de esta Ventana Modal", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores durante más de 6 horas: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas", "Reddit": "Reddit", "UnableToAddANewAppProfilePleaseTryAgain": "No se ha podido añadir un nuevo perfil de calidad, prueba otra vez.", - "DeleteIndexerProxyMessageText": "Seguro que quieres eliminar la etiqueta '{0}'?", + "DeleteIndexerProxyMessageText": "¿Seguro que quieres eliminar el proxy indexador '{name}'?", "Discord": "Discord", "Add": "Añadir", "Custom": "Personalizado", "Donations": "Donaciones", - "SearchIndexers": "Buscar películas", + "SearchIndexers": "Buscar Indexadores", "Enabled": "Habilitado", - "Grabs": "Capturar", + "Grabs": "Capturas", "Presets": "Preajustes", - "RSS": "RSS", + "Rss": "RSS", "Today": "Hoy", - "Tomorrow": "mañana", + "Tomorrow": "Mañana", "Torrent": "Torrents", - "UnableToAddANewIndexerProxyPleaseTryAgain": "No se ha podido añadir un nuevo indexer, prueba otra vez.", + "UnableToAddANewIndexerProxyPleaseTryAgain": "No se pudo añadir un nuevo proxy de indexador, por favor inténtalo de nuevo.", "Wiki": "Wiki", "Yesterday": "Ayer", "ApplicationStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores", "ApplicationStatusCheckSingleClientMessage": "Listas no disponibles debido a errores: {0}", "AllIndexersHiddenDueToFilter": "Todos los indexadores están ocultas debido al filtro aplicado.", - "DeleteApplicationMessageText": "Seguro que quieres elminiar la notificación '{0}'?", - "IndexerProxyStatusCheckAllClientMessage": "Los indexers no están disponibles debido a errores", - "IndexerProxyStatusCheckSingleClientMessage": "Indexers no disponibles debido a errores: {0}", + "DeleteApplicationMessageText": "Seguro que quieres eliminar la notificación '{name}'?", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a errores", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxies de indexador no disponibles debido a errores: {indexerProxyNames}", "NoLinks": "Sin enlaces", "AddDownloadClient": "Añadir Cliente de Descarga", "CouldNotConnectSignalR": "No se pudo conectar a SignalR, la interfaz de usuario no se actualiza", "EnableRss": "Habilitar RSS", "FeatureRequests": "Peticiones de características", - "HomePage": "Página de inicio", + "HomePage": "Página principal", "UnableToAddANewApplicationPleaseTryAgain": "No se ha podido añadir una nueva notificación, prueba otra vez.", "Filters": "Filtros", "HistoryCleanupDaysHelpText": "Ajustar a 0 para desactivar la limpieza automática", "HistoryCleanupDaysHelpTextWarning": "Los archivos en la papelera de reciclaje más antiguos que el número de días seleccionado serán limpiados automáticamente", - "OnGrab": "Al Capturar", - "OnHealthIssue": "En Problema de Salud", - "TestAllIndexers": "Comprobar Todos los Indexers", - "NotificationTriggersHelpText": "Seleccione qué eventos deben activar esta notificación", - "OnApplicationUpdate": "Al Actualizar La Aplicación", + "OnGrab": "Al Capturar lanzamiento", + "OnHealthIssue": "Al haber un problema de salud", + "TestAllIndexers": "Probar todos los indexadores", + "NotificationTriggersHelpText": "Selecciona qué eventos deberían disparar esta notificación", + "OnApplicationUpdate": "Al actualizar la aplicación", "OnApplicationUpdateHelpText": "Al Actualizar La Aplicación", "AddRemoveOnly": "Sólo añadir y eliminar", "AddedToDownloadClient": "Descarga añadida al cliente", @@ -340,98 +340,98 @@ "Categories": "Categorías", "AddIndexerProxy": "Añadir proxy del indexador", "Encoding": "Codificación", - "GrabReleases": "Capturar Estreno", - "Link": "Enlaces", - "MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulte las preguntas frecuentes para obtener más información", + "GrabReleases": "Capturar Lanzamiento(s)", + "Link": "Enlace", + "MappedDrivesRunningAsService": "Las unidades de red asignadas no están disponibles cuando se ejecutan como un servicio de Windows. Consulta las preguntas frecuentes para obtener más información", "No": "No", - "Notification": "Notificaciones", + "Notification": "Notificación", "Notifications": "Notificaciones", - "UnableToLoadIndexers": "No se pueden cargar los indexers", + "UnableToLoadIndexers": "No se pueden cargar los indexadores", "Yes": "Sí", - "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación llamó a la API", - "InstanceName": "Nombre de Instancia", - "InstanceNameHelpText": "Nombre de instancia en pestaña y para nombre de aplicación en Syslog", - "Database": "Base de Datos", + "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación que llamó a la API", + "InstanceName": "Nombre de la Instancia", + "InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog", + "Database": "Base de datos", "Duration": "Duración", - "LastDuration": "Duración", - "LastExecution": "Última ejecución", + "LastDuration": "Última Duración", + "LastExecution": "Última Ejecución", "Queued": "Encolado", - "ApplicationLongTermStatusCheckAllClientMessage": "Ningún indexer está disponible por errores durando más de 6 horas", - "ApplicationLongTermStatusCheckSingleClientMessage": "Indexers no disponible por errores durando más de 6 horas: {0}", - "Ended": "Terminó", + "ApplicationLongTermStatusCheckAllClientMessage": "Todas las aplicaciones no están disponibles debido a fallos por más de 6 horas", + "ApplicationLongTermStatusCheckSingleClientMessage": "Aplicaciones no disponibles por fallos por más de 6 horas: {0}", + "Ended": "Terminado", "NextExecution": "Siguiente ejecución", "Started": "Iniciado", "Remove": "Eliminar", "Replace": "Reemplazar", - "TheLatestVersionIsAlreadyInstalled": "La última versión de {appName} ya está instalada", + "OnLatestVersion": "La última versión de {appName} ya está instalada", "Apps": "Aplicaciones", "AddApplication": "Añadir aplicación", - "AddCustomFilter": "Añadir filtro personalizado", + "AddCustomFilter": "Añadir Filtro Personalizado", "Description": "Descripción", "ApplicationURL": "URL de la aplicación", "ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base", "Label": "Etiqueta", "Theme": "Tema", - "ApplyTagsHelpTextAdd": "Añadir: Añadir a las etiquetas la lista existente de etiquetas", - "DeleteSelectedApplicationsMessageText": "Seguro que quieres eliminar el indexer '{0}'?", - "DeleteSelectedDownloadClients": "Borrar Gestor de Descargas", - "DeleteSelectedIndexersMessageText": "Seguro que quieres eliminar el indexer '{0}'?", - "DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {0} cliente(s) de descarga seleccionado(s)?", - "ApplyTagsHelpTextHowToApplyApplications": "Cómo añadir etiquetas a las películas seleccionadas", - "SelectIndexers": "Buscar películas", - "ApplyTagsHelpTextHowToApplyIndexers": "Cómo añadir etiquetas a los indexadores seleccionados", - "ApplyTagsHelpTextRemove": "Eliminar: Eliminar las etiquetas introducidas", - "ApplyTagsHelpTextReplace": "Reemplazar: Reemplazar las etiquetas con las etiquetas introducidas (no introducir etiquetas para eliminar todas las etiquetas)", - "ThemeHelpText": "Cambia el tema de la interfaz de usuario de la aplicación. El tema \"automático\" utilizará el tema de tu sistema operativo para establecer el modo claro u oscuro. Inspirado por Theme.Park", - "DownloadClientPriorityHelpText": "Priorizar múltiples Gestores de Descargas. Se usa Round-Robin para gestores con la misma prioridad.", + "ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente", + "DeleteSelectedApplicationsMessageText": "¿Está seguro de querer eliminar {count} aplicación(es) de seleccionada(s)?", + "DeleteSelectedDownloadClients": "Borrar cliente(s) de descarga", + "DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?", + "DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?", + "ApplyTagsHelpTextHowToApplyApplications": "Cómo aplicar etiquetas a las aplicaciones seleccionadas", + "SelectIndexers": "Seleccionar Indexadores", + "ApplyTagsHelpTextHowToApplyIndexers": "Cómo aplicar etiquetas a los indexadores seleccionados", + "ApplyTagsHelpTextRemove": "Eliminar: Elimina las etiquetas introducidas", + "ApplyTagsHelpTextReplace": "Reemplazar: Sustituye las etiquetas por las introducidas (introduce \"no tags\" para borrar todas las etiquetas)", + "ThemeHelpText": "Cambia el tema de la interfaz de usuario de la aplicación. El tema \"automático\" utilizará el tema de tu sistema operativo para establecer el modo claro u oscuro. Inspirado por {inspiredBy}.", + "DownloadClientPriorityHelpText": "Priorizar varios Clientes de Descarga. Round-Robin se utiliza para clientes con la misma prioridad.", "Season": "Temporada", "More": "Más", - "Track": "Rastro", + "Track": "Pista", "Year": "Año", - "UpdateAvailable": "La nueva actualización está disponible", - "Genre": "Géneros", + "UpdateAvailableHealthCheckMessage": "Una nueva actualización está disponible: {version}", + "Genre": "Género", "Publisher": "Editor", "AuthenticationRequired": "Autenticación requerida", "ApplyChanges": "Aplicar Cambios", - "CountIndexersSelected": "{0} indexador(es) seleccionado(s)", - "CountDownloadClientsSelected": "{0} cliente(s) de descarga seleccionado(s)", + "CountIndexersSelected": "{count} indexador(es) seleccionado(s)", + "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", "EditSelectedDownloadClients": "Editar Clientes de Descarga Seleccionados", "EditSelectedIndexers": "Editar Indexadores Seleccionados", "Implementation": "Implementación", - "ManageDownloadClients": "Gestionar Clientes de Descarga", - "ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {0} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración", - "IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {0}.", + "ManageDownloadClients": "Administrar Clientes de Descarga", + "ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración", + "IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {indexerNames}.", "Episode": "Episodio", - "ConnectionLostReconnect": "Radarr intentará conectarse automáticamente, o haz clic en el botón de recarga abajo.", - "ConnectionLostToBackend": "Radarr ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.", + "ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puedes pulsar en recargar abajo.", + "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.", "RecentChanges": "Cambios recientes", - "WhatsNew": "¿Qué hay de nuevo?", - "minutes": "Minutos", - "Album": "álbum", - "Artist": "artista", - "DeleteAppProfileMessageText": "Seguro que quieres eliminar el perfil de calidad {0}", + "WhatsNew": "¿Qué hay nuevo?", + "minutes": "minutos", + "Album": "Álbum", + "Artist": "Artista", + "DeleteAppProfileMessageText": "¿Estás seguro de que quieres eliminar el perfil de la aplicación '{name}'?", "AddConnection": "Añadir Conexión", - "NotificationStatusAllClientHealthCheckMessage": "Las listas no están disponibles debido a errores", - "NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {0}", - "EditIndexerImplementation": "Agregar Condición - { implementationName}", + "NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a errores", + "NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponibles debido a errores: {notificationNames}", + "EditIndexerImplementation": "Editar Indexador - {implementationName}", "AuthBasic": "Básico (ventana emergente del navegador)", - "AuthForm": "Formularios (página de inicio de sesión)", + "AuthForm": "Formularios (Página de inicio de sesión)", "Author": "Autor", "Book": "Libro", "Clone": "Clonar", - "DisabledForLocalAddresses": "Deshabilitado para direcciones locales", + "DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales", "External": "Externo", - "None": "Ninguna", - "ResetAPIKeyMessageText": "¿Está seguro de que desea restablecer su clave API?", - "EditIndexerProxyImplementation": "Agregar Condición - { implementationName}", - "AppUpdated": "{appName} Actualizada", - "AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes, necesitaras recargar {appName}", - "AddApplicationImplementation": "Agregar Condición - { implementationName}", + "None": "Ninguno", + "ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?", + "EditIndexerProxyImplementation": "Editar proxy de indexador - {implementationName}", + "AppUpdated": "{appName} Actualizado", + "AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes tendrás que recargar {appName}", + "AddApplicationImplementation": "Agregar aplicación - {implementationName}", "AddConnectionImplementation": "Añadir Conexión - {implementationName}", - "AddIndexerImplementation": "Agregar Condición - { implementationName}", - "AddIndexerProxyImplementation": "Agregar Condición - { implementationName}", - "EditApplicationImplementation": "Agregar Condición - { implementationName}", - "EditConnectionImplementation": "Agregar Condición - { implementationName}", + "AddIndexerImplementation": "Agregar Indexador - {implementationName}", + "AddIndexerProxyImplementation": "Agregar Proxy de Indexador - { implementationName}", + "EditApplicationImplementation": "Editar aplicación - {implementationName}", + "EditConnectionImplementation": "Editar Conexión - {implementationName}", "AddDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", "AuthenticationMethod": "Método de autenticación", "AuthenticationMethodHelpTextWarning": "Por favor selecciona un método válido de autenticación", @@ -439,7 +439,373 @@ "AuthenticationRequiredPasswordHelpTextWarning": "Introduzca una nueva contraseña", "AuthenticationRequiredUsernameHelpTextWarning": "Introduzca un nuevo nombre de usuario", "AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación esté habilitada. Opcionalmente puede desactivar la autenticación desde una dirección local.", - "EditDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", + "EditDownloadClientImplementation": "Editar Cliente de Descarga - {implementationName}", "DefaultNameCopiedProfile": "{name} - Copia", - "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña" + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", + "NoHistoryFound": "No se encontró historial", + "DeleteApplication": "Eliminar Aplicación", + "AreYouSureYouWantToDeleteCategory": "Esta seguro que desea eliminar la categoría mapeada?", + "AdvancedSettingsHiddenClickToShow": "Configuraciones avanzadas escondidas, click para mostrar", + "AdvancedSettingsShownClickToHide": "Se muestran las configuraciones avanzadas, click para esconder", + "AppsMinimumSeeders": "Semillas mínimas para las Aplicaciones", + "ClearHistoryMessageText": "Esta seguro que desea borrar todo el historial de {appName}?", + "ClearHistory": "Borrar Historial", + "AreYouSureYouWantToDeleteIndexer": "Esta seguro que desea eliminar '{name}' de {appName}?", + "AuthQueries": "Consultas de Autorización", + "ApplicationTagsHelpText": "Sincronizar indexadores con esta aplicación que tengan una o más etiquetas coincidentes. Si no aparece ninguna etiqueta, no se impedirá la sincronización de ningún indexador debido a sus etiquetas.", + "ApplicationTagsHelpTextWarning": "Las etiquetas deben utilizarse con cuidado, pueden tener efectos involuntarios. Una aplicación con una etiqueta solo sincronizara con Indexadores que tengan la misma etiqueta.", + "AppsMinimumSeedersHelpText": "Cantidad mínima de semillas requerida por las Aplicaciones para que el Indexador capture, vacío aplica la configuración por defecto del perfil", + "AverageResponseTimesMs": "Tiempo promedio de respuesta del Indexador (ms)", + "CountIndexersAvailable": "{count} indexadores disponibles", + "DeleteAppProfile": "Eliminar Perfil de Aplicación", + "AddSyncProfile": "Agregar Perfil de Sincronización", + "AppSettingsSummary": "Aplicaciones y configuraciones para determinar como {appName} interactúa con sus programas de PVR", + "AddCategory": "Agregar categoría", + "AppProfileSelectHelpText": "Los perfiles de la aplicación se usan para controlar la configuración por aplicación de RSS, Búsqueda Automática y Búsqueda Interactiva", + "ActiveApps": "Aplicaciones activas", + "ActiveIndexers": "Indexadores activos", + "AudioSearch": "Búsqueda de Música", + "Auth": "Autenticación", + "BasicSearch": "Búsqueda Básica", + "CountApplicationsSelected": "{count} aplicaciones seleccionadas", + "IndexerHealthCheckNoIndexers": "Ningún indexador habilitado, {appName} no devolverá resultados de búsqueda", + "IndexerAuth": "Autentificación de indexador", + "DeleteSelectedApplications": "Eliminar aplicaciones seleccionadas", + "DeleteSelectedIndexer": "Eliminar indexador seleccionado", + "IndexerCategories": "Categorías de indexador", + "IndexerDownloadClientHelpText": "Especifique qué cliente de descarga se utiliza para capturas hechas desde {appName} a partir de este indexador", + "FullSync": "Sincronización total", + "GoToApplication": "Ir a la aplicación", + "FoundCountReleases": "{itemCount} releases hallados", + "HistoryDetails": "Detalles de historial", + "DeleteClientCategory": "Eliminar categoría de cliente de descarga", + "DeleteSelectedIndexers": "Eliminar indexadores seleccionados", + "DevelopmentSettings": "Ajustes de desarrollo", + "EnabledRedirected": "Habilitado, redireccionado", + "IndexerDetails": "Detalles de indexador", + "IndexerDisabled": "Indexador deshabilitado", + "IndexerFailureRate": "Tasa de fallo del indexador", + "IndexerAlreadySetup": "Hay al menos una instancia de indexador configurada", + "DisabledUntil": "Deshabilitado hasta", + "DownloadClientCategory": "Categoría de cliente de descarga", + "HistoryCleanup": "Limpieza de historial", + "Id": "ID", + "EditCategory": "Editar categoría", + "EditSyncProfile": "Editar perfil de sincronización", + "EnableIndexer": "Habilitar indexador", + "InvalidUILanguage": "Tu interfaz de usuario está configurada en un idioma inválido, corrígelo y guarda la configuración", + "DownloadClientQbittorrentSettingsContentLayout": "Diseño del contenido", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usa el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crea una subcarpeta (qBittorrent 4.3.2+)", + "EnableRssHelpText": "Habilitar canal RSS para el Indexador", + "days": "días", + "ElapsedTime": "Tiempo transcurrido", + "GrabTitle": "Capturar título", + "SearchAllIndexers": "Buscar en todos los Indexadores", + "Website": "Sitio web", + "Redirect": "Redirección", + "RssQueries": "Consultas RSS", + "SeedRatio": "Proporción de Semillado", + "RssFeed": "Canal RSS", + "SearchType": "Tipo de búsqueda", + "RepeatSearch": "Repetir búsqueda", + "SeedRatioHelpText": "El ratio que un torrent debe alcanzar antes de detenerse, si está vacío se usará el valor por defecto", + "SeedTime": "Tiempo de Semillado", + "SearchTypes": "Tipos de búsquedas", + "DeleteIndexerProxy": "Eliminar proxy indexador", + "OnGrabHelpText": "Al capturar lanzamiento", + "SeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de detenerse, si está vacío usa el valor predeterminado", + "IndexerTagsHelpTextWarning": "Las etiquetas deben utilizarse con precaución, ya que pueden tener efectos no deseados. Un indexador con una etiqueta solo se sincronizará con aplicaciones que tengan la misma etiqueta.", + "TVSearchTypes": "Tipos de búsqueda de TV", + "DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada", + "IndexerNoDefinitionCheckHealthCheckMessage": "Los indexadores no tienen definición y no funcionarán: {indexerNames}. Por favor elimínalos y (o) vuelve a añadirlos a {appName}.", + "IndexerProxy": "Proxy del Indexador", + "IndexerObsoleteCheckMessage": "Los indexadores están obsoletos o se han actualizado: {0}. Por favor elimínelos y (o) vuelva a añadirlos a {appName}", + "IncludeManualGrabsHelpText": "Incluir las Capturas Manuales realizadas en {appName}", + "IndexerRss": "RSS del Indexador", + "IndexerSettingsSummary": "Configurar varios ajustes globales del Indexador, incluyendo Proxies.", + "IndexerName": "Nombre del Indexador", + "IndexerVipExpiringHealthCheckMessage": "Las ventajas VIP del indexador expiran pronto: {indexerNames}", + "IndexerProxies": "Proxies del Indexador", + "IndexerHistoryLoadError": "Error al cargar el historial del Indexador", + "IndexerVipExpiredHealthCheckMessage": "Las ventajas VIP del indexador han expirado: {indexerNames}", + "IndexerQuery": "Consulta dedl Indexador", + "IndexerSite": "Sitio del Indexador", + "IndexerStatus": "Estado del indexador", + "LastFailure": "Último Fallo", + "InitialFailure": "Fallo Inicial", + "MappedCategories": "Categorías Mapeadas", + "IndexerTagsHelpText": "Utilice etiquetas para especificar los Proxies del Indexador o las aplicaciones con las que se sincroniza el indexador.", + "MassEditor": "Editor Masivo", + "ApplicationsLoadError": "No se puede cargar la lista de aplicaciones", + "ManageClients": "Administrar Clientes", + "ManageApplications": "Gestionar Aplicaciones", + "IndexerId": "ID del Indexador", + "IndexerInfo": "Información del Indexador", + "SyncProfiles": "Sincronizar Perfiles", + "TestAllApps": "Comprobar Todas las Aplicaciones", + "Query": "Consulta", + "SettingsIndexerLogging": "Registro mejorado del Indexador", + "Private": "Privado", + "QueryOptions": "Opciones de Consulta", + "NewznabUrl": "Url Newznab", + "QueryType": "Tipo de Consulta", + "SearchCountIndexers": "Buscar en {count} indexador(es)", + "SelectedCountOfCountReleases": "Seleccionados {selectedCount} de {itemCount} lanzamientos", + "SyncLevel": "Nivel de Sincronización", + "StopSelecting": "Detener la Selección", + "MinimumSeeders": "Semillas mínimas", + "QueryResults": "Resultados de la Consulta", + "SyncAppIndexers": "Sincronizar Indexadores de Aplicación", + "NotificationsEmailSettingsUseEncryption": "Usar Cifrado", + "OnHealthRestoredHelpText": "Al resolver las incidencias", + "OnHealthRestored": "Al resolver las incidencias", + "PackSeedTime": "Tiempo de Semillado del Pack", + "SearchCapabilities": "Capacidades de Búsqueda", + "SearchQueries": "Consultas de búsqueda", + "SettingsLogRotateHelpText": "Número máximo de archivos de registro que se guardan en la carpeta de registros", + "Stats": "Estadísticas", + "SyncLevelAddRemove": "Sólo Añadir y Eliminar: Cuando se añadan o eliminen indexadores de {appName}, se actualizará esta aplicación remota.", + "Public": "Publico", + "NotSupported": "No soportado", + "Proxies": "Proxies", + "SettingsConsoleLogLevel": "Nivel de Registro de la Consola", + "SettingsFilterSentryEvents": "Filtrar Eventos Analíticos", + "Parameters": "Parámetros", + "RedirectHelpText": "Redirigir la solicitud de descarga entrante para el indexador y pasar la captura directamente en lugar de delegar la solicitud a través de {appName}", + "SyncProfile": "Sincronizar Perfil", + "TorznabUrl": "Url Torznab", + "MovieSearchTypes": "Tipos de Búsqueda de Películas", + "NoDownloadClientsFound": "No se han encontrado clientes de descarga", + "NoIndexerHistory": "No se ha encontrado ningún historial para este indexador", + "PackSeedTimeHelpText": "El tiempo que un pack (temporada o discografía) debe ser sembrado antes de que se detenga, el valor vacío es el valor por defecto de la aplicación", + "SettingsLogSql": "Registrar Sql", + "SettingsSqlLoggingHelpText": "Registrar todas las consultas SQL de {appName}", + "SettingsLogRotate": "Rotación de Registros", + "TotalGrabs": "Capturas Totales", + "SettingsIndexerLoggingHelpText": "Registrar datos adicionales del Indexador, incluida la respuesta", + "MusicSearchTypes": "Tipos de Búsqueda de Música", + "TotalIndexerQueries": "Consultas Totales por Indexador", + "SyncLevelFull": "Sincronización completa: Se mantendrán los indexadores de esta aplicación totalmente sincronizados. Los cambios realizados en los indexadores de {appName} se sincronizarán con esta aplicación. Cualquier cambio realizado en los indexadores de forma remota dentro de esta aplicación será anulado por {appName} en la siguiente sincronización.", + "NoApplicationsFound": "No se han encontrado aplicaciones", + "RawSearchSupported": "Búsqueda en crudo soportada", + "SemiPrivate": "Semi-Privado", + "NoIndexerCategories": "No se han encontrado categorías para este indexador", + "MovieSearch": "Búsqueda de Películas", + "MinimumSeedersHelpText": "Mínimo de semillas requeridas por la Aplicación para que el Indexador coja la descarga", + "NoIndexersFound": "No se han encontrado indexadores", + "Privacy": "Privacidad", + "TotalHostQueries": "Consultas Totales por Host", + "PasswordConfirmation": "Confirmación de Contraseña", + "SettingsFilterSentryEventsHelpText": "Filtrar eventos de error de usuario conocidos para que no se envíen como Análisis", + "TotalHostGrabs": "Capturas Totales por Host", + "NoSearchResultsFound": "No se han encontrado resultados de búsqueda, intente realizar una nueva búsqueda abajo.", + "TotalUserAgentGrabs": "Capturas Totales Por Agente de Usuario", + "UnableToLoadIndexerProxies": "No se pueden cargar los Proxies de Indexador", + "UnableToLoadAppProfiles": "No se pueden cargar los perfiles de las aplicaciones", + "TotalUserAgentQueries": "Consultas Totales Por Agente de Usuario", + "TvSearch": "Buscar series de TV", + "UnableToLoadDevelopmentSettings": "No se pueden cargar los ajustes de Desarrollo", + "TotalQueries": "Consultas Totales", + "Url": "Url", + "VipExpiration": "Expiración VIP", + "TotalIndexerSuccessfulGrabs": "Capturas con Éxito Totales por Indexador", + "NotificationsEmailSettingsUseEncryptionHelpText": "Si prefiere utilizar el cifrado si está configurado en el servidor, utilizar siempre el cifrado mediante SSL (sólo puerto 465) o StartTLS (cualquier otro puerto), o no utilizar nunca el cifrado", + "IndexerHDBitsSettingsPasskeyHelpText": "Clave de acceso desde los Detalles de Usuario", + "IndexerSettingsPasskey": "Clave de Acceso", + "BlackholeFolderHelpText": "La carpeta donde {appName} almacenará los archivos {extension}", + "CustomFilter": "Filtro personalizado", + "LabelIsRequired": "Se requiere etiqueta", + "TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet", + "TorrentBlackholeTorrentFolder": "Carpeta de torrent", + "UseSsl": "Usar SSL", + "UsenetBlackholeNzbFolder": "Carpeta Nzb", + "TorrentBlackholeSaveMagnetFilesHelpText": "Guarda el enlace magnet si no hay ningún archivo .torrent disponible (útil solo si el cliente de descarga soporta magnets guardados en un archivo)", + "SecretToken": "Token secreto", + "TorrentBlackholeSaveMagnetFilesExtension": "Guardar extensión de archivos magnet", + "Donate": "Donar", + "ClickToChangeQueryOptions": "Haz clic para cambiar las opciones de petición", + "DefaultCategory": "Categoría predeterminada", + "Destination": "Destino", + "Directory": "Directorio", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo al url del json de deluge, vea {url}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada", + "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades de medios como etiquetas. Los consejos son ejemplos.", + "DownloadClientFreeboxSettingsAppId": "ID de la app", + "DownloadClientFreeboxSettingsAppToken": "Token de la app", + "DownloadClientFreeboxSettingsApiUrl": "URL de API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID de la app dada cuando se crea acceso a la API de Freebox (esto es 'app_id')", + "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciales de una descarga. Para ser reconocida, una descarga debe tener todas las etiquetas iniciales. Esto evita conflictos con descargas no relacionadas.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')", + "ProwlarrDownloadClientsAlert": "Si intentas hacer búsquedas directamente dentro de {appName}, necesitas añadir clientes de descarga. De otro modo, no necesitas añadirlos aquí. Para búsquedas desde tus aplicaciones, los clientes de descarga configurados serán usados en su lugar.", + "ProwlarrDownloadClientsInAppOnlyAlert": "Los clientes de descarga son solo para búsquedas internas en {appName} y no sincronizan a las aplicaciones. No hay planes para añadir cualquier funcionalidad.", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, predeterminado a '.magnet'", + "DownloadClientRTorrentSettingsUrlPath": "Ruta de url", + "IndexerHDBitsSettingsOrigins": "Orígenes", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent asociado con la cookie usada desde el navegador", + "IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de parar, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sincronizar rechazo de hashes de torrents en la lista de bloqueo mientras se captura", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Debes tener permisos de usuario y de torrents", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Busca lanzamientos por nombres de grupo", + "IndexerHDBitsSettingsOriginsHelpText": "Si no se especifica, se usarán todas las opciones.", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie de User-Agent", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Excluye lanzamientos de ESCENA de los resultados", + "IndexerAlphaRatioSettingsExcludeScene": "Excluir ESCENA", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerBeyondHDSettingsLimitedOnly": "Solo limitados", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Buscar solo rebobinados", + "IndexerBeyondHDSettingsRssKeyHelpText": "Clave RSS del sitio (encontrada en Mi seguridad => Clave RSS)", + "IndexerBeyondHDSettingsSearchTypes": "Tipos de búsqueda", + "IndexerGazelleGamesSettingsSearchGroupNames": "Buscar nombres de grupo", + "IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usarán todas las opciones.", + "IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usarán todas las opciones.", + "IndexerHDBitsSettingsUseFilenames": "Usar nombres de archivo", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Señala esta opción si quieres usar nombres de archivo como títulos de lanzamiento", + "IndexerHDBitsSettingsUsernameHelpText": "Usuario del sitio", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Parámetros adicionales de Newznab", + "IndexerNewznabSettingsApiKeyHelpText": "Clave API del sitio", + "IndexerNzbIndexSettingsApiKeyHelpText": "Clave API del sitio", + "IndexerPassThePopcornSettingsApiUserHelpText": "Estas opciones se encuentran en tus opciones de seguridad de PassThePopcorn (Editar perfil > Seguridad).", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Clave API del sitio", + "IndexerRedactedSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Opciones => Opciones de acceso)", + "IndexerSettingsAdditionalParameters": "Parámetros adicionales", + "IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}", + "IndexerSettingsApiUser": "Usuario de API", + "IndexerSettingsAppsMinimumSeeders": "Semillas mínimas de las aplicaciones", + "IndexerSettingsBaseUrlHelpText": "Selecciona qué url base usará {appName} para las peticiones al sitio", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsRssKey": "Clave RSS", + "IndexerSettingsSeedRatio": "Ratio de sembrado", + "IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de parar, vacío usa el predeterminado del cliente de descarga", + "IndexerMTeamTpSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Panel de control de usuario => Seguridad => Laboratorio)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "Menu": "Menú", + "Mixed": "Mezclado", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla", + "DownloadClientRTorrentSettingsAddStopped": "Añadir detenido", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Categoría devuelta por defecto si no existe ninguna categoría mapeada para un lanzamiento. Añadir una categoría específica a {appName} evita conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.", + "DownloadClientSettingsAddPaused": "Añadir pausado", + "DownloadClientSettingsPriorityItemHelpText": "Prioridad a usar cuando se capturen elementos", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}", + "DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission", + "DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}", + "IndexerBeyondHDSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Mi seguridad => Clave API)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Buscar solo freeleech (UL limitada)", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerFileListSettingsPasskeyHelpText": "Clave de acceso del sitio (Esto es la cadena alfanumérica en la url del tracker mostrada en tu cliente de descarga)", + "IndexerFileListSettingsUsernameHelpText": "Usuario del sitio", + "IndexerHDBitsSettingsMediums": "Medios", + "IndexerHDBitsSettingsCodecs": "Códecs", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostrar solo lanzamientos freeleech", + "IndexerNebulanceSettingsApiKeyHelpText": "Clave API de Opciones de usuario > Claves API. La clave debe tener permisos de lista y descarga", + "IndexerNewznabSettingsVipExpirationHelpText": "Introduce la fecha (yyyy-mm-dd) para Expiración VIP o en blanco, {appName} notificará a una semana de la expiración del VIP", + "IndexerOrpheusSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Opciones => Opciones de acceso)", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerSettingsApiPath": "Ruta de API", + "IndexerSettingsBaseUrl": "Url base", + "IndexerSettingsPackSeedTime": "Tiempo de sembrado de pack", + "IndexerSettingsLimitsUnit": "Unidad de límites", + "IndexerSettingsQueryLimit": "Límite de petición", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Clave API del sitio", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Si un torrent es bloqueado por un hash puede no ser rechazado apropiadamente durante Buscar/RSS para algunos indexadores, habilitar esto permitirá que sea rechazado después de que el torrent sea capturado, pero antes es enviado al cliente.", + "IndexerSettingsAppsMinimumSeedersHelpText": "Semillas mínimas requeridas por las aplicaciones para que el indexador capture, dejar vacío usa el predeterminado de Sincronización de perfil", + "DownloadClientSettingsDefaultCategoryHelpText": "Categoría devuelta por defecto si no existe ninguna categoría mapeada para un lanzamiento. Añadir una categoría específica a {appName} evita conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero bastante recomendado.", + "IndexerSettingsGrabLimitHelpText": "El número de capturas máximas especificadas por la unidad respectiva que {appName} permitirá al sitio", + "IndexerSettingsLimitsUnitHelpText": "La unidad o tiempo para límites de recuento por indexador", + "IndexerSettingsPackSeedTimeIndexerHelpText": "El tiempo en que un pack de torrent (temporada o discografía) debería ser compartido antes de detenerse, vacío usa el predeterminado de la aplicación", + "DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone", + "DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial", + "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)", + "DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.", + "DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado", + "DownloadClientSettingsInitialState": "Estado inicial", + "IndexerBeyondHDSettingsRefundOnly": "Solo devueltos", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Buscar solo devueltos", + "IndexerBeyondHDSettingsRewindOnly": "Solo rebobinados", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Selecciona los tipos de lanzamientos en los que estás interesado. Si no se selecciona ninguno, se usarán todas las opciones.", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Clave API del sitio (encontrada en Opciones => Opciones de acceso)", + "IndexerSettingsGrabLimit": "Límite de captura", + "IndexerSettingsQueryLimitHelpText": "El número de peticiones máximas especificadas por la unidad respectiva que {appName} permitirá al sitio", + "IndexerSettingsSeedTime": "Tiempo de sembrado", + "IndexerSettingsCookieHelpText": "Cookie del sitio", + "IndexerSettingsFreeleechOnly": "Solo freeleech", + "IndexerSettingsVipExpiration": "Expiración del VIP", + "XmlRpcPath": "Ruta RPC de XML", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Busca solo lanzamientos freeleech", + "ProxyValidationBadRequest": "Fallo al probar el proxy. Código de estado: {statusCode}", + "ProxyValidationUnableToConnect": "No se pudo conectar al proxy: {exceptionMessage}. Comprueba el registro de este error para más detalles", + "GrabRelease": "Capturar lanzamiento", + "ManualGrab": "Captura manual", + "Open": "Abrir", + "OverrideAndAddToDownloadClient": "Sobrescribir y añadir al cliente de descarga", + "OverrideGrabModalTitle": "Sobrescribir y capturar - {title}", + "PrioritySettings": "Prioridad: {priority}", + "SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga", + "Default": "Predeterminado", + "BuiltIn": "Integrado", + "Script": "Script", + "Any": "Cualquiera", + "Redirected": "Redirección", + "InfoUrl": "Información de la URL", + "PublishedDate": "Fecha de publicación", + "AverageQueries": "Promedio de peticiones", + "AverageGrabs": "Promedio de capturas", + "AllSearchResultsHiddenByFilter": "Todos los resultados están ocultos por el filtro aplicado.", + "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "HealthMessagesInfoBox": "Puedes encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte en los enlaces que aparecen abajo.", + "LogSizeLimit": "Límite de tamaño de registro", + "LogSizeLimitHelpText": "Máximo tamaño de archivo de registro en MB antes de archivarlo. Predeterminado es 1MB.", + "PreferMagnetUrl": "Preferir URL magnet", + "IndexerSettingsPreferMagnetUrl": "Preferir URL magnet", + "IndexerSettingsPreferMagnetUrlHelpText": "Cuando está habilitado, este indexador preferirá el uso de URL magnet para capturas con alternativas a enlaces torrent", + "PreferMagnetUrlHelpText": "Cuando está habilitado, este indexador preferirá el uso de URL magnet para capturas con alternativas a enlaces torrent", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Solo Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Busca lanzamientos solo en Golden Popcorn", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Buscar solo lanzamientos freeleech", + "IndexerAvistazSettingsUsernameHelpText": "Nombre de usuario del sitio", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Solo los miembros de rango y superiores pueden usar la API en este indexador.", + "IndexerAvistazSettingsPasswordHelpText": "Contraseña del sitio", + "IndexerAvistazSettingsPidHelpText": "PID de la página de Mi cuenta o Mi perfil", + "LogFilesLocation": "Los archivos de registro se encuentran en: {location}", + "DockerUpdater": "Actualiza el contenedor docker para recibir la actualización", + "Download": "Descargar", + "ErrorRestoringBackup": "Error restaurando la copia de seguridad", + "ExternalUpdater": "{appName} está configurado para usar un mecanismo de actualización externo", + "FailedToFetchUpdates": "Fallo al buscar las actualizaciones", + "Logout": "Cerrar Sesión", + "NoEventsFound": "Ningún evento encontrado", + "RestartReloadNote": "Nota: {appName} se reiniciará automáticamente y recargará la interfaz durante el proceso de restauración.", + "TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)", + "UpdateAppDirectlyLoadError": "No se pudo actualizar {appName} directamente,", + "UpdaterLogFiles": "Actualizador de archivos de registro", + "WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?", + "AptUpdater": "Usa apt para instalar la actualización", + "Install": "Instalar", + "InstallLatest": "Instala el último", + "InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?", + "InstallMajorVersionUpdate": "Instalar actualización", + "InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.", + "FailedToFetchSettings": "Error al recuperar la configuración", + "CurrentlyInstalled": "Actualmente instalado", + "PreviouslyInstalled": "Previamente instalado", + "DownloadClientUTorrentProviderMessage": "uTorrent tiene un amplio historial de incluir criptomineros, malware y publicidad, por lo que recomendamos encarecidamente que elijas un cliente diferente." } diff --git a/src/NzbDrone.Core/Localization/Core/fa.json b/src/NzbDrone.Core/Localization/Core/fa.json index 0967ef424..c0f9f6513 100644 --- a/src/NzbDrone.Core/Localization/Core/fa.json +++ b/src/NzbDrone.Core/Localization/Core/fa.json @@ -1 +1,13 @@ -{} +{ + "ApiKey": "کلید API", + "NetCore": ".NET", + "Add": "افزودن", + "About": "درباره", + "Actions": "اقدامات", + "Docker": "Docker", + "AddConnection": "افزودن پیوند", + "AddConnectionImplementation": "افزودن پیوند - {implementationName}", + "AddDownloadClientImplementation": "افزودن کلاینت دانلود - {implementationName}", + "Torrents": "تورنت ها", + "Usenet": "Usenet" +} diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 19b60958a..0236ce555 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1,15 +1,15 @@ { - "IndexerProxyStatusCheckSingleClientMessage": "Välityspalvelimet eivät ole käytettävissä virheiden vuoksi: {0}", - "Logging": "Kirjaus", - "LogLevel": "Kirjauksen taso", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Välityspalvelimet eivät ole käytettävissä virheiden vuoksi: {indexerProxyNames}", + "Logging": "Lokikirjaus", + "LogLevel": "Lokikirjauksen laajuus", "MovieIndexScrollTop": "Elokuvakirjasto: vieritä ylös", "Apply": "Käytä", - "ClientPriority": "Lataustyökalun painotus", - "IndexerPriorityHelpText": "Tietolähteen painotus: 1 (korkein) - 50 (matalin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen sieppauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", + "ClientPriority": "Latauspalvelun painotus", + "IndexerPriorityHelpText": "Hakupalvelun painotus, 1– 50 (korkein-alin). Oletusarvo on 25.", "Manual": "Manuaalinen", "Add": "Lisää", "Reload": "Lataa uudelleen", - "Indexers": "Tietolähteet", + "Indexers": "Hakupalvelut", "MovieIndexScrollBottom": "Elokuvakirjasto: vieritä alas", "SSLCertPassword": "SSL-varmenteen salasana", "Style": "Ulkoasu", @@ -25,240 +25,239 @@ "SettingsTimeFormat": "Kellonajan esitys", "Message": "Viesti", "Seeders": "Jakajat", - "TestAll": "Testaa kaikki", - "AddDownloadClient": "Lisää lataustyökalu", - "CustomFilters": "Mukautetut suodattimet", + "TestAll": "Koesta kaikki", + "AddDownloadClient": "Lisää latauspalvelu", + "CustomFilters": "Omat suodattimet", "DeleteTag": "Poista tunniste", - "EnableRss": "RSS-syöte", - "Filter": "Suodata", + "EnableRss": "Käytä RSS-syötettä", + "Filter": "Suodatus", "Fixed": "Korjattu", "FocusSearchBox": "Kohdista hakukenttä", - "ForMoreInformationOnTheIndividualDownloadClients": "Lue lisää lataustyökalusta painamalla 'Lisätietoja'.", - "HideAdvanced": "Piilota edistyneet", + "ForMoreInformationOnTheIndividualDownloadClients": "Saat lisätietoja yksittäisistä latauspalveluista painamalla niiden ohessa olevia lisätietopainikkeita.", + "HideAdvanced": "Laajenna asetukset", "History": "Historia", "MIA": "Puuttuu", - "New": "Uusi", - "PageSizeHelpText": "Sivulla näytettävien kohteiden määrä", + "New": "Uutta", + "PageSizeHelpText": "Sivukohtainen kohdemäärä.", "Proxy": "Välityspalvelin", - "ProxyBypassFilterHelpText": "Käytä erottimena ',' ja '*.' jokerimerkkinä aliverkkotunnuksille (esim. www.esimerkki.fi,*.esimerkki.fi)", + "ProxyBypassFilterHelpText": "Erota aliverkkotunnukset pilkuilla ja käytä jokerimerkkinä tähteä ja pistettä (*.). Esimerkki: www.esimerkki.fi,*.esimerkki.fi).", "Reddit": "Reddit", "Refresh": "Päivitä", "RefreshMovie": "Päivitä elokuva", - "ReleaseBranchCheckOfficialBranchMessage": "'{0}' ei ole kelvollinen {appName}-julkaisuhaara, etkä saa päivityksiä sen kautta", - "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii uudelleenkäynnistyksen.", + "ReleaseBranchCheckOfficialBranchMessage": "\"{0}\" ei ole kelvollinen {appName}-julkaisuhaara ja tämän vuoksi et saa päivityksiä.", + "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii sovelluksen uudelleenkäynnistyksen.", "Result": "Tulos", "Settings": "Asetukset", "SettingsLongDateFormat": "Pitkän päiväyksen esitys", "SettingsShortDateFormat": "Lyhyen päiväyksen esitys", - "UnselectAll": "Poista kaikkien valinta", - "UpdateCheckStartupTranslocationMessage": "Päivitystä ei voi asentaa, koska käynnistyskansio '{0}' sijaitsee 'App Translocation' -kansiossa.", - "UpdateCheckUINotWritableMessage": "Päivitystä ei voi asentaa, koska käyttäjällä '{1}' ei ole kirjoitusoikeutta käyttöliittymäkansioon '{0}'.", - "UpdateMechanismHelpText": "Käytä {appName}in sisäänrakennettua päivitystoimintoa tai omaa komentosarjaasi.", + "UnselectAll": "Tyhjennä valinnat", + "UpdateStartupTranslocationHealthCheckMessage": "Päivitystä ei voida asentaa, koska käynnistyskansio \"{startupFolder}\" sijaitsee \"App Translocation\" -kansiossa.", + "UpdateUiNotWritableHealthCheckMessage": "Päivityksen asennus ei onnistu, koska käyttäjällä {userName} ei ole kirjoitusoikeutta käyttöliittymäkansioon \"{uiFolder}\".", + "UpdateMechanismHelpText": "Käytä {appName}in sisäänrakennettua päivitystoimintoa tai komentosarjaa.", "Enable": "Käytä", "UI": "Käyttöliittymä", - "UrlBaseHelpText": "Käänteisen välityspalvelimen tuki (esim. \"http://[host]:[port]/[urlBase]\"). Käytä oletusta jättämällä tyhjäksi.", "Usenet": "Usenet", "BackupNow": "Varmuuskopioi nyt", - "NoBackupsAreAvailable": "Varmuuskopioita ei ole saatavilla", - "UpdateCheckStartupNotWritableMessage": "Päivitystä ei voi asentaa, koska käyttäjällä '{1}' ei ole kirjoitusoikeutta käynnistyskansioon '{0}'.", + "NoBackupsAreAvailable": "Varmuuskopioita ei ole käytettävissä", + "UpdateStartupNotWritableHealthCheckMessage": "Päivitystä ei voida asentaa, koska käyttäjällä {userName} ei ole kirjoitusoikeutta käynnistyskansioon \"{startupFolder}\".", "Updates": "Päivitykset", "UpdateScriptPathHelpText": "Polku komentosarjaan, joka käsittelee puretun päivitystiedoston ja hoitaa asennuksen loppuosuuden.", "Uptime": "Käyttöaika", "URLBase": "URL-perusta", "UseProxy": "Käytä välityspalvelinta", "Username": "Käyttäjätunnus", - "YesCancel": "Kyllä, peruuta", + "YesCancel": "Kyllä, peru", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "ApplyTags": "Tunnistetoimenpide", - "Authentication": "Todennus", - "AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana {appName}in käyttöön.", - "BindAddressHelpText": "Toimiva IP-osoite, \"localhost\" tai \"*\" (tähti) kaikille verkkoliitännöille.", + "Authentication": "Tunnistautuminen", + "AuthenticationMethodHelpText": "Vaadi {appName}in käyttöön käyttäjätunnus ja salasana.", + "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", "Close": "Sulje", - "DeleteNotification": "Poista kytkentä", + "DeleteNotification": "Poista ilmoituspalvelu", "Docker": "Docker", - "DownloadClient": "Lataustyökalu", + "DownloadClient": "Latauspalvelu", "Language": "Kieli", "Search": "Haku", "Details": "Tiedot", - "InteractiveSearch": "Vuorovaikutteinen haku", - "Interval": "Aikaväli", + "InteractiveSearch": "Etsi manuaalisesti", + "Interval": "Ajoitus", "KeyboardShortcuts": "Pikanäppäimet", - "LastWriteTime": "Viimeisin kirjoitusaika", + "LastWriteTime": "Edellinen tallennus", "LogFiles": "Lokitiedostot", - "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulisi käyttää vain väliaikaisesti", + "LogLevelTraceHelpTextWarning": "Jäljityskirjausta tulee käyttää vain tilapäisesti.", "Logs": "Lokitiedot", "Mechanism": "Mekanismi", "Name": "Nimi", - "NoLinks": "Ei linkkejä", + "NoLinks": "Kytköksiä ei ole", "Peers": "Vertaiset", "Presets": "Esiasetukset", "Priority": "Painotus", "Protocol": "Protokolla", - "ProxyCheckBadRequestMessage": "Välityspalvelintesti epäonnistui. Tilakoodi: {0}", - "ProxyCheckFailedToTestMessage": "Välityspalvelintesti epäonnistui: {0}", - "ProxyCheckResolveIpMessage": "Määritetyn välityspalvelimen \"{0}\" IP-osoitteen selvitys epäonnistui.", - "ProxyPasswordHelpText": "Käyttäjätunnus ja salasana tulee syöttää vain tarvittaessa. Muussa tapauksessa jätä kentät tyhjiksi.", + "ProxyBadRequestHealthCheckMessage": "Välityspalvelintesti epäonnistui. Tilakoodi: {statusCode}.", + "ProxyFailedToTestHealthCheckMessage": "Välityspalvelintesti epäonnistui: {url}", + "ProxyResolveIpHealthCheckMessage": "Määritetyn välityspalvelimen \"{proxyHostName}\" IP-osoitteen selvitys epäonnistui.", + "ProxyPasswordHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", "ProxyType": "Välityspalvelimen tyyppi", - "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee syöttää vain tarvittaessa. Muussa tapauksessa jätä kentät tyhjiksi.", + "ProxyUsernameHelpText": "Käyttäjätunnus ja salasana tulee täyttää vain tarvittaessa. Mikäli näitä ei ole, tulee kentät jättää tyhjiksi.", "Queue": "Jono", - "ReadTheWikiForMoreInformation": "Lue lisätietoja Wikistä", + "ReadTheWikiForMoreInformation": "Wikistä löydät lisää tietoja", "ReleaseStatus": "Julkaisutila", "RemovedFromTaskQueue": "Poistettu tehtäväjonosta", "RemoveFilter": "Poista suodatin", "RemovingTag": "Tunniste poistetaan", "Reset": "Uudista", - "ResetAPIKey": "Uudista API-avain", + "ResetAPIKey": "Korvaa rajapinnan avain", "Restart": "Käynnistä uudelleen", "RestartNow": "Käynnistä uudelleen nyt", "Restore": "Palauta", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS-syötettä ei ole käytettävissä tälle tietolähteelle", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "Tämän hakupalvelun kanssa ei voida käyttää RSS-syötettä.", "ScriptPath": "Komentosarjan sijainti", "Security": "Suojaus", "SuggestTranslationChange": "Ehdota käännösmuutosta", "System": "Järjestelmä", - "SystemTimeCheckMessage": "Järjestelmän aika on pielessä yli vuorokauden. Ajoitetut tehtävät eivät luultavasti toimi oikein ennen sen korjausta.", - "TagCannotBeDeletedWhileInUse": "Tunnistetta ei voi poistaa, koska se on käytössä", - "TagIsNotUsedAndCanBeDeleted": "Tunnistetta ei ole määritetty millekään kohteelle, joten sen voi poistaa.", - "TagsSettingsSummary": "Täältä näet kaikki tunnisteet käyttökohteineen ja voit poistaa sellaiset tunnisteet, joita ei ole määritetty millekään kohteelle.", + "SystemTimeHealthCheckMessage": "Järjestelmän aika on ainakin vuorokauden pielessä, eivätkä ajoitetut tehtävät 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.", + "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": "Kokeile", - "TestAllClients": "Testaa kaikki lataustyökalut", + "Test": "Koesta", + "TestAllClients": "Koesta palvelut", "Time": "Aika", - "Title": "Nimi", + "Title": "Nimike", "Tomorrow": "Huomenna", "Torrent": "Torrent", "Torrents": "Torrentit", "Type": "Tyyppi", "UILanguage": "Käyttöliittymän kieli", - "UnableToAddANewApplicationPleaseTryAgain": "Uuden sovelluksen lisäys epäonnistui. Yritä uudelleen.", - "UnableToAddANewIndexerPleaseTryAgain": "Uuden tietolähteen lisäys epäonnistui. Yritä uudelleen.", - "UnableToAddANewIndexerProxyPleaseTryAgain": "Uuden tietolähdevälityspalvelimen lisäys epäonnistui. Yritä uudelleen.", - "UnableToLoadBackups": "Varmuuskopioiden lataus epäonnistui.", - "UnableToLoadDownloadClients": "Lataustyökalujen lataus epäonnistui.", - "UnableToLoadGeneralSettings": "Yleisten asetusten lataus epäonnistui.", - "UpdateAutomaticallyHelpText": "Lataa ja asenna päivitykset automaattisesti. Voit edelleen asentaa ne myös lähteestä System:Updates.", - "Added": "Lisätty", - "AddIndexer": "Lisää tietolähde", + "UnableToAddANewApplicationPleaseTryAgain": "Virhe lisättäessä sovellusta. Yritä uudelleen.", + "UnableToAddANewIndexerPleaseTryAgain": "Uuden hakupalvelun lisääminen epäonnistui. Yritä uudelleen.", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Virhe lisättäessä tiedonhaun välityspalvelinta. Yritä uudelleen.", + "BackupsLoadError": "Virhe ladattaessa varmuuskopioita.", + "DownloadClientsLoadError": "Virhe ladattaessa latauspalveluita.", + "UnableToLoadGeneralSettings": "Yleisasetusten lataus epäonnistui", + "UpdateAutomaticallyHelpText": "Lataa ja asenna päivitykset automaattisesti. Voit myös edelleen suorittaa asennuksen järjestelmäasetusten päivitykset-osiosta.", + "Added": "Lisäysaika", + "AddIndexer": "Lisää hakupalvelu", "AddingTag": "Tunniste lisätään", "Age": "Ikä", "All": "Kaikki", - "AllIndexersHiddenDueToFilter": "Aktiivinen suodatin on piilottanut kaikki tietolähteet.", + "AllIndexersHiddenDueToFilter": "Aktiivinen suodatin on piilottanut kaikki hakupalvelut.", "Analytics": "Analytiikka", - "AnalyticsEnabledHelpText": "Lähetä nimettömiä käyttö- ja virhetietoja palvelimillemme. Tämä sisältää tietoja selaimestasi, käyttöliittymän sivujen käytöstä, virheraportoinnista, käyttöjärjestelmästä ja suoritusalustasta. Käytämme näitä tietoja ominaisuuksien ja vikakorjausten painotukseen.", - "ApiKey": "API-avain", + "AnalyticsEnabledHelpText": "Lähetä nimettömiä käyttö- ja virhetietoja {appName}in palvelimille. Tämä sisältää tietoja selaimestasi, käyttöliittymän sivujen käytöstä, virheraportoinnista, käyttöjärjestelmästä ja suoritusalustasta. Käytämme näitä tietoja ominaisuuksien ja vikakorjausten painotukseen.", + "ApiKey": "Rajapinnan avain", "AppDataDirectory": "AppData-kansio", - "DBMigration": "Tietokannan siirto", + "DatabaseMigration": "Tietokannan siirto", "Delete": "Poista", - "DeleteIndexerProxyMessageText": "Haluatko varmasti poistaa välityspalvelimen '{0}'?", - "DeleteNotificationMessageText": "Haluatko varmasti poistaa kytkennän '{0}'?", + "DeleteIndexerProxyMessageText": "Haluatko varmasti poistaa hakupalveluvälityspalvelimen \"{name}\"?", + "DeleteNotificationMessageText": "Haluatko varmasti poistaa ilmoituspalvelun \"{name}\"?", "Disabled": "Ei käytössä", - "DownloadClients": "Lataustyökalut", - "DownloadClientSettings": "Lataustyökalujen asetukset", - "DownloadClientStatusCheckAllClientMessage": "Yhtään lataustyökalua ei ole virheiden vuoksi käytettävissä", + "DownloadClients": "Latauspalvelut", + "DownloadClientSettings": "Latauspalveluasetukset", + "DownloadClientStatusAllClientHealthCheckMessage": "Latauspalveluita ei ole ongelmien vuoksi käytettävissä", "Mode": "Tila", "MoreInfo": "Lisätietoja", "SelectAll": "Valitse kaikki", "SendAnonymousUsageData": "Lähetä nimettömiä käyttötietoja", "SetTags": "Tunnisteiden määritys", "SettingsEnableColorImpairedMode": "Heikentyneen värinäön tila", - "ShowAdvanced": "Näytä lisäasetukset", + "ShowAdvanced": "Supista asetukset", "ShowSearchHelpText": "Näytä hakupainike osoitettaessa.", "Shutdown": "Sammuta", "Size": "Koko", - "Sort": "Järjestä", - "UnableToAddANewDownloadClientPleaseTryAgain": "Uuden lataustyökalun lisäys epäonnistui. Yitä uudelleen.", - "AppDataLocationHealthCheckMessage": "Päivitystä ei sallita, jotta AppData-kansion poisto päivityksen yhteydessä voidaan estää.", - "UnableToLoadHistory": "Historian lataus epäonnistui.", - "UnableToLoadNotifications": "Kytkentöjen lataus epäonnistui.", - "UnableToLoadTags": "Tunnisteiden lataus epäonnistui.", - "UnableToLoadUISettings": "Käyttöliittymän asetuksien lataus epäonnistui.", - "UnsavedChanges": "Tallentamattomia muutoksia", + "Sort": "Järjestys", + "UnableToAddANewDownloadClientPleaseTryAgain": "Latauspalvelun lisääminen epäonnistui. Yritä uudelleen.", + "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää", + "UnableToLoadHistory": "Virhe ladattaessa historiaa.", + "UnableToLoadNotifications": "Virhe ladattaessa ilmoituspalveluita.", + "UnableToLoadTags": "Virhe ladattaessa tunnisteita.", + "UnableToLoadUISettings": "Virhe ladattaessa käyttöliittymäasetuksia.", + "UnsavedChanges": "Muutoksia ei ole tallennettu", "Yesterday": "Eilen", - "ConnectionLost": "Yhteys on katkennut", - "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa lataustyökalun '{0}'?", - "DeleteTagMessageText": "Haluatko varmasti poistaa tunnisteen '{0}'?", + "ConnectionLost": "Yhteys menetettiin", + "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa latauspalvelun \"{name}\"?", + "DeleteTagMessageText": "Haluatko varmasti poistaa tunnisteen \"{label}\"?", "Discord": "Discord", "Donations": "Lahjoitukset", "Edit": "Muokkaa", "EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihauille, jotka suoritetaan käyttöliittymästä tai {appName}in toimesta.", "Enabled": "Käytössä", - "EventType": "Tapahtumatyyppi", + "EventType": "Tapahtuman tyyppi", "Exception": "Poikkeus", "FeatureRequests": "Kehitysehdotukset", - "Grabbed": "Siepattu", + "Grabbed": "Kaapattu", "IgnoredAddresses": "Ohitetut osoitteet", "IllRestartLater": "Käynnistän uudelleen myöhemmin", - "Info": "Tiedot", + "Info": "Informatiivinen", "LaunchBrowserHelpText": " Avaa {appName}in verkkokäyttöliittymä verkkoselaimeen sovelluksen käynnistyksen yhteydessä.", - "NoChanges": "Ei muutoksia", + "NoChanges": "Muutoksia ei ole", "NoLeaveIt": "Ei, anna olla", - "PendingChangesMessage": "On tallentamattomia muutoksia. Haluatko varmasti poistua sivulta?", + "PendingChangesMessage": "Olet tehnyt muutoksia, joita ei ole vielä tallennettu. Haluatko varmasti poistua sivulta?", "PendingChangesStayReview": "Älä poistu ja tarkista muutokset", "Save": "Tallenna", "SaveChanges": "Tallenna muutokset", "SaveSettings": "Tallenna asetukset", - "Scheduled": "Ajoitettu", - "SettingsEnableColorImpairedModeHelpText": "Vaihtoehtoinen tyyli, joka auttaa erottamaan värikoodatut tiedot paremmin", + "Scheduled": "Ajoitukset", + "SettingsEnableColorImpairedModeHelpText": "Vaihtoehtoinen tyyli, joka auttaa erottamaan värikoodatut tiedot paremmin.", "SettingsShowRelativeDates": "Suhteellisten päiväysten esitys", - "SettingsShowRelativeDatesHelpText": "Näytä suhteutetut (tänään/eilen/yms.) absoluuttisten sijaan", + "SettingsShowRelativeDatesHelpText": "Korvaa absoluuttiset päiväykset suhteellisilla päiväyksillä (tänään/eilen/yms.).", "ShowSearch": "Näytä haku", "Source": "Lähde", "SSLPort": "SSL-portti", "StartTypingOrSelectAPathBelow": "Aloita kirjoitus tai valitse sijainti alta", "StartupDirectory": "Käynnistyskansio", - "TableOptions": "Taulukkoasetukset", - "TableOptionsColumnsMessage": "Valitse näytettävät sarakkeet ja niiden järjestys", - "TagsHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille tietolähteille. Käytä kaikille jättämällä tyhjäksi.", - "UnableToAddANewAppProfilePleaseTryAgain": "Uuden sovellusprofiilin lisäys epäonnistui. Yritä uudelleen.", - "UnableToAddANewNotificationPleaseTryAgain": "Kytkennän lisäys epäonnistui. Yritä uudelleen.", + "TableOptions": "Taulukkonäkymän asetukset", + "TableOptionsColumnsMessage": "Valitse näytettävät sarakkeet ja niiden järjestys.", + "TagsHelpText": "Käytetään vähintään yhdellä täsmäävällä tunnisteella merkityille hakupalveluille.", + "UnableToAddANewAppProfilePleaseTryAgain": "Virhe lisättäessä sovellusprofiilia. Yritä uudelleen.", + "UnableToAddANewNotificationPleaseTryAgain": "Ilmoituspalvelun lisääminen epäonnistui. Yritä uudelleen.", "Version": "Versio", - "View": "Näytä", + "View": "Näkymä", "Warn": "Varoita", "Wiki": "Wiki", "Port": "Portti", "Automatic": "Automaattinen", "AutomaticSearch": "Automaattihaku", - "Backup": "Varmuuskopio", - "BackupFolderHelpText": "Suhteelliset polut kohdistuvat sovelluksen AppData-kansioon.", - "BackupIntervalHelpText": "Automaattisen varmuuskopioinnin aikaväli", - "BackupRetentionHelpText": "Säilytysjaksoa vanhemmat automaattiset varmuuskopiot poistetaan automaattisesti.", + "Backup": "Varmuuskopiointi", + "BackupFolderHelpText": "Suhteelliset tiedostosijainnit ovat {appName}in AppData-kansiossa.", + "BackupIntervalHelpText": "Tietokannan ja asetusten automaattisen varmuuskopioinnin ajoitus.", + "BackupRetentionHelpText": "Säilytysaikaa vanhemmat varmuuskopiot siivotaan automaattisesti.", "Backups": "Varmuuskopiot", "BeforeUpdate": "Ennen päivitystä", "BindAddress": "Sidososoite", - "Branch": "Kehityshaara", - "BranchUpdate": "Sovelluksen versiopäivityksiin käytettävä kehityshaara.", + "Branch": "Haara", + "BranchUpdate": "{appName}in versiopäivityksiin käytettävä kehityshaara.", "BranchUpdateMechanism": "Ulkoisen päivitysratkaisun käyttämä kehityshaara.", "BypassProxyForLocalAddresses": "Ohjaa paikalliset osoitteet välityspalvelimen ohi", "Cancel": "Peruuta", - "CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?", + "CancelPendingTask": "Haluatko varmasti perua odottavan tehtävän?", "CertificateValidation": "Varmenteen vahvistus", - "CertificateValidationHelpText": "Muuta HTTPS-varmennevahvistuksen tarkkuutta. Älä muuta, jollet ymmärrä tähän liittyviä riskejä.", + "CertificateValidationHelpText": "Määritä HTTPS-varmennevahvistuksen tiukkuus. Älä muuta, jos et ymmärrä riskejä.", "ChangeHasNotBeenSavedYet": "Muutosta ei ole vielä tallennettu", "Clear": "Tyhjennä", - "CloneProfile": "Kloonaa profiili", + "CloneProfile": "Monista profiili", "CloseCurrentModal": "Sulje nykyinen ikkuna", "Columns": "Sarakkeet", "Component": "Komponentti", - "Connections": "Kytkennät", - "ConnectSettings": "Kytkentöjen asetukset", - "CouldNotConnectSignalR": "SignalR-kirjastoa ei tavoitettu, eikä käyttöliittymää päivitetä", + "Connections": "Ilmoituspalvelut", + "ConnectSettings": "Ilmoituspavelun asetukset", + "CouldNotConnectSignalR": "SignalR-kirjastoa ei tavoitettu, eikä käyttöliittymä päivity.", "Custom": "Mukautettu", - "DeleteApplicationMessageText": "Haluatko varmasti poistaa sovelluksen \"{0}\"?", + "DeleteApplicationMessageText": "Haluatko varmasti poistaa sovelluksen \"{name}\"?", "DeleteBackup": "Poista varmuuskopio", - "DeleteBackupMessageText": "Haluatko varmasti poistaa varmuuskopion '{0}'?", - "DeleteDownloadClient": "Poista lataustyökalu", - "DownloadClientStatusCheckSingleClientMessage": "Lataustyökalut eivät ole virheiden vuoksi käytettävissä: {0}", - "EditIndexer": "Muokkaa tietolähdettä", - "EnableAutomaticSearch": "Automaattihaku", - "EnableInteractiveSearch": "Vuorovaikutteinen haku", - "EnableInteractiveSearchHelpText": "Profiilia käytetään vuorovaikutteisen haun yhteydessä.", + "DeleteBackupMessageText": "Haluatko varmasti poistaa varmuuskopion \"{name}\"?", + "DeleteDownloadClient": "Poista latauspalvelu", + "DownloadClientStatusSingleClientHealthCheckMessage": "Latauspalveluita ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", + "EditIndexer": "Muokkaa hakupalvelua", + "EnableAutomaticSearch": "Käytä automaattihakua", + "EnableInteractiveSearch": "Käytä manuaalihakuun", + "EnableInteractiveSearchHelpText": "Profiilia käytetään manuaalihakuun.", "EnableSSL": "SSL-salaus", - "EnableSslHelpText": " Käyttöönotto edellyttää uudelleenkäynnistystä järjestelmänvalvojan oikeuksilla.", + "EnableSslHelpText": " Käyttöönotto vaatii uudelleenkäynnistyksen järjestelmänvalvojan oikeuksilla.", "Error": "Virhe", - "ErrorLoadingContents": "Sisällönlatauksen virhe", + "ErrorLoadingContents": "Virhe ladattaessa sisältöjä", "Events": "Tapahtumat", - "ExistingTag": "Olemassa oleva tunniste", + "ExistingTag": "Tunniste on jo olemassa", "Failed": "Epäonnistui", "Filename": "Tiedostonimi", "Files": "Tiedostot", @@ -266,84 +265,84 @@ "General": "Yleiset", "GeneralSettings": "Yleiset asetukset", "GeneralSettingsSummary": "Portti, SSL-salaus, käyttäjänimi ja salasana, välityspalvelin, analytiikka ja päivitykset.", - "Grabs": "Sieppaukset", - "Health": "Kunto", + "Grabs": "Kaappaukset", + "Health": "Terveys", "Level": "Taso", - "HealthNoIssues": "Kokoonpanossasi ei ole ongelmia.", + "NoIssuesWithYourConfiguration": "Kokoonpanossasi ei ole ongelmia.", "HomePage": "Verkkosivusto", "Host": "Osoite", "Hostname": "Osoite", "IncludeHealthWarningsHelpText": "Sisällytä kuntovaroitukset", - "Indexer": "Tietolähde", - "IndexerFlags": "Tietolähteen liput", - "IndexerLongTermStatusCheckAllClientMessage": "Mikään tietolähde ei ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.", - "IndexerLongTermStatusCheckSingleClientMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}", - "IndexerPriority": "Tietolähteiden painotus", - "IndexerProxyStatusCheckAllClientMessage": "Välityspalvelimet eivät ole käytettävissä virheiden vuoksi", - "IndexerStatusCheckAllClientMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi", - "IndexerStatusCheckSingleClientMessage": "Tietolähteet eivät ole käytettävissä virheiden vuoksi: {0}", + "Indexer": "Hakupalvelu", + "IndexerFlags": "Hakupalvelun liput", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Mikään hakupalvelu ei ole käytettävissä yli kuusi tuntia kestäneiden virheiden vuoksi.", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Hakupalvelut eivät ole käytettävissä yli kuusi tuntia kestäneiden virheiden vuoksi: {indexerNames}.", + "IndexerPriority": "Hakupalveluiden painotus", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Välityspalvelimet eivät ole käytettävissä virheiden vuoksi", + "IndexerStatusAllUnavailableHealthCheckMessage": "Hakupalvelut eivät ole virheiden vuoksi käytettävissä.", + "IndexerStatusUnavailableHealthCheckMessage": "Hakupalvelut eivät ole virheiden vuoksi käytettävissä: {indexerNames}.", "NoChange": "Ei muutosta", - "NoLogFiles": "Ei lokitiedostoja", + "NoLogFiles": "Lokitiedostoja ei ole", "SSLCertPasswordHelpText": "PFX-tiedoston salasana", "SSLCertPath": "SSL-varmenteen sijainti", "SSLCertPathHelpText": "PFX-tiedoston sijainti", "Status": "Tila", - "NotificationTriggers": "Laukaisijat", + "NotificationTriggers": "Ilmoituksen laukaisijat", "NoUpdatesAreAvailable": "Päivityksiä ei ole saatavilla", "OAuthPopupMessage": "Selaimesi estää ponnahdukset", "Ok": "Ok", - "OnHealthIssueHelpText": "Kuntoon liittyvä ongelma", + "OnHealthIssueHelpText": "Vakausongelmat", "OpenBrowserOnStart": "Avaa selain käynnistettäessä", "OpenThisModal": "Avaa tämä ikkuna", - "Options": "Valinnat", - "PackageVersion": "Pakettiversio", + "Options": "Asetukset", + "PackageVersion": "Paketin versio", "PageSize": "Sivun koko", "Password": "Salasana", "PendingChangesDiscardChanges": "Hylkää muutokset ja poistu", - "PortNumber": "Portti", + "PortNumber": "Portin numero", "RestoreBackup": "Palauta varmuuskopio", "Retention": "Säilytys", - "UILanguageHelpText": "Käyttöliittymä näytetään tällä kielellä.", - "UILanguageHelpTextWarning": "Käyttöönotto vaatii selaimen sivupäivityksen.", + "UILanguageHelpText": "{appName}in käyttöliittymän kieli.", + "UILanguageHelpTextWarning": "Vaatii selaimen sivupäivityksen (F5).", "UISettings": "Käyttöliittymän asetukset", - "DownloadClientsSettingsSummary": "{appName}in käyttöliittymästä suoritettavien hakujen yhteydessä käytettävien lataustyökalujen määritykset.", - "ProwlarrSupportsAnyDownloadClient": "{appName} tukee alla listatuja lataustyökaluja.", - "AddDownloadClientToProwlarr": "Lisäämällä lataustyökalun {appName} voi käynnistää lataukset suoraan käyttöliittymästä manuaalisen haun yhteydessä.", - "RedirectHelpText": "Uudelleenohjaa tietolähteeltä saapuvat latauspyynnöt ja ohjaa sieppaus suoraan sen sijaan, että se välitettäisiin Prowlarin kautta.", + "DownloadClientsSettingsSummary": "{appName}in käyttöliittymästä suoritettavien hakujen yhteydessä käytettävät latauspalvelut.", + "ProwlarrSupportsAnyDownloadClient": "{appName} tukee kaikkia alla listatuja latauspalveluita.", + "AddDownloadClientToProwlarr": "Lisäämällä latauspalvelun {appName} voi lähettää julkaisut suoraan käyttöliittymästä manuaalihaun tuloksista.", + "RedirectHelpText": "Uudelleenohjaa hakupalvelulta saapuvat latauspyynnöt ja välitä kaappaus suoraan välittämättä sitä {appName}in kautta.", "FullSync": "Täysi synkronointi", - "SyncLevelFull": "Täysi synkronointi: Pitää sovelluksen tietolähteet täysin synkronoituna. Tietolähteisiin {appName}issa tehdyt muutokset synkronoidaan etäsovelluksen kanssa ja kaikki etäsovelluksessa tehdyt muutokset korvataan seuraavan synkronoinnin yhteydessä.", - "EnableIndexer": "Tietolähteen tila", - "FilterPlaceHolder": "Suodata tietolähteitä", - "IndexerHealthCheckNoIndexers": "Yhtään tietolähdettä ei ole käytössä, eikä {appName} tämän vuoksi löydä tuloksia.", - "IndexerObsoleteCheckMessage": "Tietolähteet ovat vanhentuneita tai niitä on päivitetty: {0}. Poista ja/tai lisää ne Prowlariin uudelleen.", - "IndexerProxy": "Tietolähteen välityspalvelin", - "IndexerSettingsSummary": "Määritä useita globaaleita tietolähdeasetuksia, kuten välityspalvelimia.", - "IndexerVipCheckExpiringClientMessage": "Tietolähteen VIP-edut erääntyvät pian: {0}", - "ProwlarrSupportsAnyIndexer": "{appName} tukee Newznab- ja Torznab-yhteensopivien tietolähteiden ohella myös useita muita lähteitä vaihtoehdoilla \"Yleinen Newznab\" (Usenetille) ja 'Yleinen Torznab' (torrenteille).", - "SettingsIndexerLogging": "Tehostettu tietolähteiden valvonta", + "SyncLevelFull": "Täysi synkronointi: Pitää sovelluksen hakupalvelut täysin synkronoituna. Hakupalveluihin {appName}issa tehdyt muutokset synkronoidaan etäsovelluksen kanssa ja kaikki etäsovelluksessa tehdyt muutokset korvataan seuraavan synkronoinnin yhteydessä.", + "EnableIndexer": "Ota hakupalvelu käyttöön", + "FilterPlaceHolder": "Suodata palveluita", + "IndexerHealthCheckNoIndexers": "Yhtään hakupalvelua ei ole käytössä, eikä {appName} tämän vuoksi löydä tuloksia.", + "IndexerObsoleteCheckMessage": "Hakupalvelut ovat poistuneet tai ne ovat muuttuneet: {0}. Poista tai lisää ne {appName}iin uudelleen.", + "IndexerProxy": "Tiedonhaun välityspalvelin", + "IndexerSettingsSummary": "Määritä useita globaaleita hakupalveluasetuksia, kuten välityspalvelimia.", + "IndexerVipExpiringHealthCheckMessage": "Hakupalvelun VIP-edut päättyvät pian: {indexerNames}.", + "ProwlarrSupportsAnyIndexer": "{appName} tukee Newznab- ja Torznab-yhteensopivien hakupalveluiden ohella myös useita muita palveluita vaihtoehdoilla \"Yleinen Newznab\" (Usenetille) ja 'Yleinen Torznab' (torrenteille).", + "SettingsIndexerLogging": "Tehostettu hakupalveluiden valvonta", "AddIndexerProxy": "Lisää tiedonhaun välityspalvelin", "UISettingsSummary": "Kalenterin, päiväyksen ja kellonajan sekä kielen ja heikentyneelle värinäölle sopivan tilan asetukset.", - "SettingsIndexerLoggingHelpText": "Kirjaa tarkempia tietoja tietolähteiden toiminnasta, mukaanlukien vastaukset", - "IndexerTagsHelpText": "Tunnisteiden avulla voit määrittää tiedonhaun välityspalvelimet, mihin sovelluksiin tietolähteet synkronoidaan tai yksikertaisesti järjestellä tietolähteitäsi.", - "UnableToLoadAppProfiles": "Sovellusprofiilien lataus epäonnistui", - "AppProfileSelectHelpText": "Sovellusprofiilieilla määritetään tietolähteelle sovellussynkronoinnin yhteydessä aktivoitavat hakutavat (RSS/automaatti/vuorovaikutteinen).", - "IndexerQuery": "Tietolähteen kysely", - "IndexerRss": "Tietolähteen RSS-syöte", - "SearchIndexers": "Etsi tietolähteistä", + "SettingsIndexerLoggingHelpText": "Kirjaa tarkempia tietoja hakupalveluiden toiminnasta, mukaanlukien vastaukset", + "IndexerTagsHelpText": "Tunnisteilla voit kohdistaa tiedonhaun välityspalvelimia ja määrittää mihin sovelluksiin ne synkronoidaan.", + "UnableToLoadAppProfiles": "Virhe ladattaessa sovellusprofiileja.", + "AppProfileSelectHelpText": "Sovellusprofiilieilla määritetään hakupalvelulle sovellussynkronoinnin yhteydessä aktivoitavat hakutavat (RSS/automaatti/manuaali).", + "IndexerQuery": "Hakupalvelukysely", + "IndexerRss": "Hakupalvelun RSS", + "SearchIndexers": "Etsi hakupalveluista", "AddRemoveOnly": "Ainoastaan lisää/poista", - "IndexerVipCheckExpiredClientMessage": "Tietolähteen VIP-edut ovat erääntyneet: {0}", - "MaintenanceRelease": "Huoltojulkaisu: Korjauksia ja muita parannuksia. Lue lisää Githubin historiasta muutoshistoriasta.", + "IndexerVipExpiredHealthCheckMessage": "Hakupalvelun VIP-edut ovat päättyneet: {indexerNames}.", + "MaintenanceRelease": "Huoltojulkaisu: korjauksia ja muita parannuksia. Lue lisää Githubin muutoshistoriasta.", "Query": "Kysely", "Redirect": "Uudelleenohjaus", "RestartProwlarr": "Käynnistä {appName} uudelleen", "SyncLevel": "Synkronoinnin laajuus", - "SyncLevelAddRemove": "Vain lisäys/poisto: Kun {appName}in tietolähteitä lisätään tai poistetaan, päivittyy myös etäsovellus.", - "SyncAppIndexers": "Synkronoi tietolähteet", - "TestAllApps": "Testaa kaikki sovellukset", - "UnableToLoadIndexerProxies": "Tietolähdevälityspalvelimien lataus epäonnistui", - "AddedToDownloadClient": "Julkaisu lisättiin lataustyökaluun", - "AddNewIndexer": "Lisää uusi tietolähde", - "AddToDownloadClient": "Lisää julkaisu lataustyökaluun", + "SyncLevelAddRemove": "Vain lisäys/poisto: Kun {appName}in hakupalveluita lisätään tai poistetaan, päivittyy myös tämä etäsovellus.", + "SyncAppIndexers": "Synkronoi sovelluksiin", + "TestAllApps": "Koesta sovellukset", + "UnableToLoadIndexerProxies": "Virhe ladattaessa tiedonhaun välityspalvelimia.", + "AddedToDownloadClient": "Julkaisu lisättiin latauspalveluun", + "AddNewIndexer": "Lisää uusi hakupalvelu", + "AddToDownloadClient": "Lisää julkaisu latauspalveluun", "NoSearchResultsFound": "Tuloksia ei löytynyt. Yritä uutta hakua alta.", "Notification": "Ilmoitus", "DeleteIndexerProxy": "Poista tiedonhaun välityspalvelin", @@ -352,30 +351,30 @@ "SettingsLogRotate": "Lokitiedostojen kierrätys", "SettingsLogSql": "Kirjaa SQL", "SettingsSqlLoggingHelpText": "Kirjaa kaikki {appName}in SQL-kyselyt", - "ConnectSettingsSummary": "Ilmoitukset ja omat komentosarjat.", + "ConnectSettingsSummary": "Yhteydet ilmoituspalveluihin ja mukautetut komentosarjat.", "DevelopmentSettings": "Kehittäjäasetukset", "Description": "Kuvaus", - "Id": "Tunniste", - "SettingsConsoleLogLevel": "Valvontalokin taso", + "Id": "ID", + "SettingsConsoleLogLevel": "Valvontalokin laajuus", "SettingsFilterSentryEvents": "Suodata analytiikan tapahtumia", "SettingsFilterSentryEventsHelpText": "Suodata tunnetut käyttäjävirheet pois analytiikkalähetyksistä", "Applications": "Sovellukset", "AppProfileInUse": "Sovellusprofiili on käytössä", "Apps": "Sovellukset", "Auth": "Todennus", - "Category": "Luokitus", + "Category": "Kategoria", "ClearHistory": "Tyhjennä historia", "ClearHistoryMessageText": "Haluatko varmasti tyhjentää kaiken {appName}-historian?", - "Connect": "Kytkennät", - "EnableRssHelpText": "Tietolähde sisällytetään RSS-syötteeseen.", + "Connect": "Ilmoituspalvelut", + "EnableRssHelpText": "Käytä hakupalvelulle RSS-syötettä.", "DeleteApplication": "Poista sovellus", "DeleteAppProfile": "Poista sovellusprofiili", - "IndexerProxies": "Tietolähteiden välityspalvelimet", - "IndexerAuth": "Tietolähteen todennus", - "Notifications": "Kytkennät", - "NotificationTriggersHelpText": "Valitse tapahtumat, jotka aiheuttavat ilmoituksen.", + "IndexerProxies": "Hakupalveluiden välityspalvelimet", + "IndexerAuth": "Hakupalvelun todennus", + "Notifications": "Ilmoituspalvelut", + "NotificationTriggersHelpText": "Valitse ilmoituksen laukaisevat tapahtumat.", "Stats": "Tilastot", - "UnableToLoadDevelopmentSettings": "Kehittäjäasetusten lataus epäonnistui", + "UnableToLoadDevelopmentSettings": "Virhe ladattaessa kehittäjäasetuksia.", "AppSettingsSummary": "Sovellukset ja asetukset, joilla määritetään miten {appName} viestii PVR-sovellustesi kanssa.", "Privacy": "Yksityisyys", "NetCore": ".NET", @@ -386,123 +385,426 @@ "QueryOptions": "Kyselyasetukset", "TvSearch": "Etsi televisiosarjoja", "Filters": "Suodattimet", - "OnGrab": "Kun elokuva siepataan", - "OnHealthIssue": "Kun havaitaan kuntoon liittyvä ongelma", - "HistoryCleanupDaysHelpText": "Poista automaattinen tyhjennys käytöstä asettamalla arvoksi '0'.", + "OnGrab": "Kun julkaisu kaapataan", + "OnHealthIssue": "Vakausongelmat", + "HistoryCleanupDaysHelpText": "Poista automaattinen tyhjennys käytöstä asettamalla arvoksi 0.", "HistoryCleanupDaysHelpTextWarning": "Tässä määritettyä aikaa vanhemmat tiedostot poistetaan automaattisesti roskakorista pysyvästi.", - "TestAllIndexers": "Testaa tietolähteet", - "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti sovellus, joka kommunikoi API:n kanssa", + "TestAllIndexers": "Koesta palvelut", + "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti rajapinnan kanssa viestinyt sovellus.", "Categories": "Kategoriat", "Database": "Tietokanta", "HistoryCleanup": "Historian siivous", - "IndexerAlreadySetup": "Tietolähteestä on määritetty jo ainakin yksi instanssi", - "IndexerInfo": "Tietolähteen tiedot", + "IndexerAlreadySetup": "Hakupalvelusta on määritetty jo ainakin yksi instanssi.", + "IndexerInfo": "Hakupalvelun tiedot", "MassEditor": "Massamuokkaus", - "OnApplicationUpdate": "Sovelluksen päivittyessä", - "OnApplicationUpdateHelpText": "Sovelluksen päivittyessä", + "OnApplicationUpdate": "Kun sovellus päivitetään", + "OnApplicationUpdateHelpText": "Kun sovellus päivitetään", "Proxies": "Välityspalvelimet", "Public": "Julkinen", "SemiPrivate": "Osittain yksityinen", - "UnableToLoadApplicationList": "Sovelluslistausta ei voitu ladata", + "ApplicationsLoadError": "Virhe ladattaessa sovelluslistaa.", "Url": "URL", "Website": "Verkkosivusto", - "IndexerNoDefCheckMessage": "Tietolähteillä ei ole määritystä, eivätkä ne toimi: {0}. Poista ja/tai lisää {appName}iin uudelleen", + "IndexerNoDefinitionCheckHealthCheckMessage": "Hakupalveluiden määritykset puuttuvat, eivätkä ne toimi: {indexerNames}. Poista tai lisää ne {appName}iin uudelleen.", "Private": "Yksityinen", "QueryResults": "Kyselyn tulokset", "Application": "Sovellus", - "GrabReleases": "Sieppaa julkaisu(t)", + "GrabReleases": "Kaappaa julkaisu(t)", "Link": "Linkki", - "SearchTypes": "Mitä etsitään", - "UnableToLoadIndexers": "Tietolähteiden lataus epäonnistui", + "SearchTypes": "Etsittävät tyypit", + "UnableToLoadIndexers": "Virhe ladattaessa hakupalveluita.", "Yes": "Kyllä", - "MappedDrivesRunningAsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä, kun sovellus suoritetaan Windows-palveluna. Lisätietoja löydät UKK:stä.", + "MappedDrivesRunningAsService": "Yhdistetyt verkkoasemat eivät ole käytettävissä kun sovellus suoritetaan Windows-palveluna. Saat lisätietoja UKK:sta.", "No": "Ei", - "BookSearchTypes": "Kirjojen hakutyypit", - "IndexerDetails": "Tietolähteen tiedot", - "IndexerName": "Tietolähteen nimi", - "IndexerSite": "Tietolähteen sivusto", - "MovieSearchTypes": "Elokuvien hakutyypit", - "MusicSearchTypes": "Musiikin hakutyypit", + "BookSearchTypes": "Etsittävät kirjatyypit", + "IndexerDetails": "Hakupalvelun tiedot", + "IndexerName": "Hakupalvelun nimi", + "IndexerSite": "Hakupalvelun sivusto", + "MovieSearchTypes": "Etsittävät elokuvatyypit", + "MusicSearchTypes": "Etsittävät musiikkityypit", "NotSupported": "Ei tuettu", "RawSearchSupported": "Raakahaku tuettu", "SearchCapabilities": "Hakuominaisuudet", - "TVSearchTypes": "Televisiosarjojen hakutyypit", + "TVSearchTypes": "Etsittävät sarja-/jaksotyypit", "MinimumSeeders": "Jakajien vähimmäismäärä", - "MinimumSeedersHelpText": "Sovelluksen edellyttämä jakajien vähimmäismäärä tietolähteestä kaappaukseen.", + "MinimumSeedersHelpText": "Sovelluksen edellyttämä hakupalvelusta kaapattavien kohteiden jakajien (seed) vähimmäismäärä.", "SyncProfile": "Synkronointiprofiili", "SyncProfiles": "Synkronointiprofiilit", "AddSyncProfile": "Lisää synkronointiprofiili", "EditSyncProfile": "Muokkaa synkronointiprofiilia", "InstanceName": "Instanssin nimi", - "InstanceNameHelpText": "Instanssin nimi välilehdellä ja järjestelmälokissa", - "ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasu. \"Automaattinen\" vaihtaa vaalean ja tumman tilan välillä järjestelmän teeman mukaan. Innoittanut Theme.Park.", + "InstanceNameHelpText": "Instanssin nimi välilehdellä ja järjestelmälokissa.", + "ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasua. \"Automaattinen\" vaihtaa vaalean ja tumman tilan välillä käyttöjärjestelmän teeman mukaan. Innoittanut {inspiredBy}.", "Duration": "Kesto", "ElapsedTime": "Kulunut aika", "EnabledRedirected": "Kulunut, uudelleenohjattu", "Ended": "Päättynyt", - "GrabTitle": "Sieppaa nimike", + "GrabTitle": "Kaappaa nimike", "LastExecution": "Edellinen suoritus", "LastDuration": "Edellinen kesto", "NextExecution": "Seuraava suoritus", "Parameters": "Parametrit", - "Queued": "Jonossa", + "Queued": "Lisätty jonoon", "Started": "Alkoi", "ApplicationLongTermStatusCheckAllClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.", "ApplicationLongTermStatusCheckSingleClientMessage": "Sovellukset eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}", - "AreYouSureYouWantToDeleteCategory": "Haluatko varmasti poistaa kartoitetun kategorian?", - "DeleteClientCategory": "Poista lataustyökalukategoria", - "DownloadClientCategory": "Lataustyökalukategoria", - "MappedCategories": "Kartoitetut kategoriat", - "AuthenticationRequired": "Todennus vaaditaan", + "AreYouSureYouWantToDeleteCategory": "Haluatko varmasti poistaa kohdistetun kategorian?", + "DeleteClientCategory": "Poista latauspalvelukategoria", + "DownloadClientCategory": "Latauspalvelukategoria", + "MappedCategories": "Kohdistetut kategoriat", + "AuthenticationRequired": "Vaadi tunnistautuminen", "Remove": "Poista", "Replace": "Korvaa", - "TheLatestVersionIsAlreadyInstalled": "{appName}in uusin versio on jo asennettu", - "ApplicationURL": "Sovelluksen URL-osoite", - "ApplicationUrlHelpText": "Sovelluksen ulkoinen URL-osoite, johon sisältyy http(s)://, portti ja URL-perusta.", - "Track": "Jäljitys", - "CountIndexersSelected": "{0} valittua tietolähdettä", - "DeleteSelectedDownloadClients": "Poista lataustyökalu", - "DeleteSelectedApplicationsMessageText": "Haluatko varmasti poistaa tietolähteen '{0}'?", - "DeleteSelectedDownloadClientsMessageText": "Haluatko varmasti poistaa tietolähteen '{0}'?", - "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa tietolähteen '{0}'?", + "OnLatestVersion": "Uusin {appName}-versio on jo asennettu", + "ApplicationURL": "Sovelluksen URL", + "ApplicationUrlHelpText": "Tämän sovelluksen ulkoinen URL-osoite, johon sisältyy http(s)://, portti ja URL-perusta.", + "Track": "Valvo", + "CountIndexersSelected": "{count} hakupalvelu(a) on valittu", + "DeleteSelectedDownloadClients": "Poista valitut latauspalvelu(t)", + "DeleteSelectedApplicationsMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) sovellu(sta/ksen)?", + "DeleteSelectedDownloadClientsMessageText": "Haluatko varmasti poistaa {count} valittua latauspalvelua?", + "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) hakupalvelu(n/a)?", "Publisher": "Julkasija", - "SelectIndexers": "Etsi tietolähteistä", + "SelectIndexers": "Palveluiden monivalinta", "Year": "Vuosi", - "Genre": "Tyylilajit", + "Genre": "Lajityyppi", "More": "Lisää", "Season": "Kausi", - "ApplyTagsHelpTextAdd": "– 'Lisää' syötetyt tunnisteet aiempiin tunnisteisiin", - "ApplyTagsHelpTextHowToApplyApplications": "Tunnistetoimenpiteiden selitykset:", - "ApplyTagsHelpTextHowToApplyIndexers": "Tunnisteisiin kohdistettavat toimenpiteet:", - "ApplyTagsHelpTextRemove": "- \"Poista\" tyhjentää syötetyt tunnisteet.", - "ApplyTagsHelpTextReplace": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi.", - "DownloadClientPriorityHelpText": "Tietolähteen painotus: 1 (korkein) - 50 (matalin). Oletusarvo on 25. Käytetään muutoin tasaveroisten julkaisujen sieppauspäätökseen. Kaikkia käytössä olevia tietolähteitä käytetään edelleen RSS-synkronointiin ja hakuun.", + "ApplyTagsHelpTextAdd": "– \"Lisää\" syötetyt tunnisteet aiempiin tunnisteisiin", + "ApplyTagsHelpTextHowToApplyApplications": "Tunnisteiden käyttö valituille sovelluksille:", + "ApplyTagsHelpTextHowToApplyIndexers": "Tunnisteiden käyttö valituille hakupalveluille:", + "ApplyTagsHelpTextRemove": "- \"Poista\" tyhjentää syötetyt tunnisteet", + "ApplyTagsHelpTextReplace": "– \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi.", + "DownloadClientPriorityHelpText": "Useiden latauspalveluiden painotus, 1–50 (korkein-alin). Oletusarvo on 1 ja tasaveroiset erotetaan Round-Robin-tekniikalla.", "Album": "Albumi", "Artist": "Esittäjä", "Author": "Kirjailija", "Book": "Kirja", - "UpdateAvailable": "Uusi päivitys on saatavilla", + "UpdateAvailableHealthCheckMessage": "Uusi päivitys on saatavilla: {version}", "Episode": "Jakso", - "Label": "Tunniste", + "Label": "Nimi", "Theme": "Teema", - "ConnectionLostReconnect": "{appName} pyrkii ajoittain muodostamaan yhteyden automaattisesti tai sitä voidaan yrittää manuaalisesti painamalla alta \"Lataa uudelleen\".", - "DeleteAppProfileMessageText": "Haluatko varmasti poistaa laatuprofiilin '{0}'?", - "RecentChanges": "Viimeaikaiset muutokset", + "ConnectionLostReconnect": "{appName} pyrkii ajoittain muodostamaan yhteyden automaattisesti tai voit painaa alta \"Lataa uudelleen\".", + "DeleteAppProfileMessageText": "Haluatko varmasti poistaa sovellusprofiilin \"{name}\"?", + "RecentChanges": "Uusimmat muutokset", "WhatsNew": "Mikä on uutta?", - "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja käytettävyyden palauttamiseksi se on ladattava uudelleen.", - "minutes": "Minuuttia", - "AddConnection": "Lisää yhteys", - "NotificationStatusAllClientHealthCheckMessage": "Sovellukset eivät ole käytettävissä virheiden vuoksi", - "NotificationStatusSingleClientHealthCheckMessage": "Sovellukset eivät ole käytettävissä virheiden vuoksi: {0}", + "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja se on käynnistettävä uudelleen.", + "minutes": "minuuttia", + "AddConnection": "Lisää ilmoituspavelu", + "NotificationStatusAllClientHealthCheckMessage": "Ilmoituspalvelut eivät ole ongelmien vuoksi käytettävissä.", + "NotificationStatusSingleClientHealthCheckMessage": "Ilmoituspalvelut eivät ole ongelmien vuoksi käytettävissä: {notificationNames}.", "AuthBasic": "Perus (selaimen ponnahdus)", "AuthForm": "Lomake (kirjautumissivu)", - "DisabledForLocalAddresses": "Ei käytetä paikallisille osoittelle", + "DisabledForLocalAddresses": "Ei käytössä paikallisissa osoitteissa", "None": "Ei mitään", - "ResetAPIKeyMessageText": "Haluatko varmasti uudistaa API-avaimesi?", - "TotalIndexerSuccessfulGrabs": "Onnistuneiden tietolähdesieppausten kokonaismäärä", + "ResetAPIKeyMessageText": "Haluatko varmasti korvata rajapinnan avaimen uudella?", + "TotalIndexerSuccessfulGrabs": "Onnistuneiden hakupalvelukaappausten kokonaismäärä", "AppUpdated": "{appName} on päivitetty", - "AppUpdatedVersion": "{appName} on päivitetty versioon {version} ja muutosten käyttöönottamiseksi se on ladattava uudelleen.", - "IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä lataustyökalu", - "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt todennuksen käyttöönoton. Todennus voidaan poistaa käytöstä paikallisille osoitteille.", - "TotalGrabs": "Sieppausten kokonaismäärä" + "AppUpdatedVersion": "{appName} on päivitetty versioon {version} ja muutosten käyttöönottamiseksi se on käynnistettävä uudelleen.", + "IndexerDownloadClientHelpText": "Määritä {appName}in käyttöliittymässä tästä hakupalvelusta kaapattaessa käytettävä latauspalvelu.", + "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt tunnistautumisen käyttöönoton. Paikallisilta osoitteilta se voidaan valinnaisesti poistaa käytöstä.", + "TotalGrabs": "Kaappausten kokonaismäärä", + "AddDownloadClientImplementation": "Lisätään latauspalvelua – {implementationName}", + "AddIndexerImplementation": "Lisätään hakupalvelua – {implementationName}", + "OnGrabHelpText": "Kun julkaisu kaapataan", + "ManageDownloadClients": "Hallitse palveluita", + "NoDownloadClientsFound": "Latauspalveluita ei löytynyt", + "CountDownloadClientsSelected": "{count} latauspalvelu(a) on valittu", + "EditSelectedDownloadClients": "Muokkaa valittuja latauspalveluita", + "IndexerDownloadClientHealthCheckMessage": "Hakupalvelut virheellisillä latauspalveluilla: {indexerNames}.", + "AddIndexerProxyImplementation": "Lisätään tiedonhaun välityspalvelinta – {implementationName}", + "EditIndexerProxyImplementation": "Muokataan tiedonhaun välityspalvelinta – {implementationName}", + "EditDownloadClientImplementation": "Muokataan latauspalvelua – {implementationName}", + "AddCustomFilter": "Lisää oma suodatin", + "ApplyChanges": "Toteuta muutokset", + "EditSelectedIndexers": "Muokkaa valittuja sisältölähteitä", + "NoHistoryFound": "Historiaa ei löytynyt", + "NoIndexersFound": "Palveluita ei löytynyt", + "StopSelecting": "Lopeta valitseminen", + "EditConnectionImplementation": "Muokataan ilmoituspalvelua – {implementationName}", + "AddConnectionImplementation": "Lisätään ilmoituspavelua – {implementationName}", + "DownloadClientQbittorrentSettingsContentLayout": "Sisällön rakenne", + "EditIndexerImplementation": "Muokataan hakupalvelua – {implementationName}", + "AuthenticationRequiredUsernameHelpTextWarning": "Syötä uusi käyttäjätunnus", + "DefaultNameCopiedProfile": "{name} (kopio)", + "AppsMinimumSeedersHelpText": "Sovellusten edellyttämä hakupalvelusta kaapattavien kohteiden jakajien (seed) vähimmäismäärä. Jos tyhjä, käytetään synkronointiprofiilin oletusta.", + "TotalHostGrabs": "Isännän kaappausten kokonaismäärä", + "IncludeManualGrabsHelpText": "Sisällytä {appName}in käyttöliittymästä tehdyt manuaalikaappaukset.", + "AuthenticationRequiredHelpText": "Valitse mitkä pyynnöt vaativat tunnistautumisen. Älä muuta, jos et ymmärrä riskejä.", + "TotalUserAgentGrabs": "Käyttäjäagentin kaappausten kokonaismäärä", + "AuthenticationMethodHelpTextWarning": "Valitse sopiva tunnistautumistapa", + "AuthenticationRequiredPasswordHelpTextWarning": "Syötä uusi salasana", + "AuthenticationMethod": "Tunnistautumistapa", + "Clone": "Monista", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Vahvista uusi salasana", + "EditApplicationImplementation": "Muokataan sovellusta – {implementationName}", + "AddApplicationImplementation": "Lisätään sovellusta – {implementationName}", + "InvalidUILanguage": "Käytöliittymän kielivalinta on virheellinen. Korjaa se ja tallenna asetukset.", + "SeedRatio": "Jakosuhde", + "SeedTime": "Jakoaika", + "days": "päivää", + "HistoryDetails": "Historiatiedot", + "IndexerDisabled": "Hakupalvelu ei ole käytössä", + "AdvancedSettingsShownClickToHide": "Lisäasetukset näytetään, piilota painamalla tästä.", + "AdvancedSettingsHiddenClickToShow": "Lisäasetukset on piilotettu, näytä painamalla tästä.", + "AppsMinimumSeeders": "Jakajien vähimmäismäärä", + "BasicSearch": "Perushaku", + "CountApplicationsSelected": "{count} sovellus(ta) on valittu", + "DeleteSelectedApplications": "Poista valitut sovellukset", + "DeleteSelectedIndexer": "Poista valittu hakupalvelu", + "DeleteSelectedIndexers": "Poista valitut hakupalvelut", + "Implementation": "Toteutus", + "IndexerCategories": "Hakupalvelukategoriat", + "IndexerStatus": "Hakupalvelun tila", + "ManageApplications": "Hallitse sovelluksia", + "NewznabUrl": "Newznab URL", + "PackSeedTime": "Paketin jakoaika", + "PackSeedTimeHelpText": "Aika, joka koostepaketin (kuten sarjan tuotantokauden tai esittäjän diskografian) sisältävää torrentia tulee jakaa. Käytä sovelluksen oletusta jättämällä tyhjäksi.", + "QueryType": "Kyselyn tyyppi", + "SearchAllIndexers": "Etsi kaikista hakupalveluista", + "SeedRatioHelpText": "Jakosuhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä sovelluksen oletusta jättämällä tyhjäksi.", + "TorznabUrl": "Torznab URL", + "ApiKeyValidationHealthCheckMessage": "Muuta rajapinnan (API) avain ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa.", + "OnHealthRestored": "Terveystilan vakautuessa", + "OnHealthRestoredHelpText": "Terveystilan vakautuessa", + "TotalHostQueries": "Isännän kyselyiden kokonaismäärä", + "TotalIndexerQueries": "Hakupalvelun kyselyiden kokonaismäärä", + "GoToApplication": "Siirry sovellukseen", + "AreYouSureYouWantToDeleteIndexer": "Haluatko varmasti poistaa hakupalvelun \"{name}\" sovelluksesta {appName}?", + "AuthQueries": "Todennuskyselyt", + "ApplicationTagsHelpText": "Sovellukseen synkronoidaan yhdellä tai useammalla vastaavalla tunnisteella merkityt hakupalvelut. Jos tässä ei ole tunnisteita, ei palveluiden synkronointia estetä tunnisteiden perusteella.", + "ApplicationTagsHelpTextWarning": "Tunnisteita tulee käyttää harkiten, koska niillä voi olla odottamattomia vaikutuksia. Tunnisteella merkittyyn sovellukseen synkronoidaan vain samalla tunnisteella merkityt hakupalvelut.", + "AverageResponseTimesMs": "hakupalveluiden keskimääräiset vasteajat (ms)", + "CountIndexersAvailable": "{count} hakupalvelu(a) on käytettävissä", + "FoundCountReleases": "Löydettiin {itemCount} julkaisua", + "IndexerTagsHelpTextWarning": "Tunnisteita tulee käyttää harkiten, koska niillä voi olla odottamattomia vaikutuksia. Tunnisteella merkitty hakupalvelu synkronoidaan vain samalla tunnisteella merkittyihin sovelluksiin.", + "LastFailure": "Edellinen virhe", + "SelectedCountOfCountReleases": "Valittu {selectedCount}/{itemCount} julkaisua", + "TotalQueries": "Kyselyiden kokonaismäärä", + "RssFeed": "RSS-syöte", + "VipExpiration": "VIP-erääntyy", + "DisabledUntil": "Poistettu käytöstä kunnes", + "AddApplication": "Lisää sovellus", + "AddCategory": "Lisää kategoria", + "EditCategory": "Muokkaa kategoriaa", + "IndexerId": "Hakupalvelun ID", + "ActiveIndexers": "Aktiiviset hakupalvelut", + "NoIndexerCategories": "Hakupalvelulle ei löytynyt kategorioita", + "ActiveApps": "Aktiiviset sovellukset", + "NoIndexerHistory": "Hakupalvelulle ei löytynyt historiaa", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Määrittää käytetäänkö qBittorrentista määritettyä rakennetta, torrentin alkuperäistä rakennetta vai luodaanko uusi alikansio (qBittorrent 4.3.2+).", + "External": "Ulkoinen", + "IndexerFailureRate": "Hakupalvelun virhetaajuus", + "IndexerHistoryLoadError": "Virhe ladattaessa hakupalvelun historiaa", + "InitialFailure": "Alkuperäinen virhe", + "PasswordConfirmation": "Salasanan vahvistus", + "RepeatSearch": "Toista haku", + "SearchCountIndexers": "Etsi {count} hakupalvelusta", + "SearchQueries": "Hakukyselyt", + "SeedTimeHelpText": "Aika, joka torrentia tulee jakaa ennen sen pysäytystä. Käytä sovelluksen oletusta jättämällä tyhjäksi.", + "RssQueries": "RSS-kyselyt", + "TotalUserAgentQueries": "Käyttäjäagentin kyselyiden kokonaismäärä", + "NotificationsEmailSettingsUseEncryption": "Käytä salausta", + "NotificationsEmailSettingsUseEncryptionHelpText": "Määrittää suositaanko salausta, jos se on määritetty palvelimelle, käytetäänkö aina SSL- (vain portti 465) tai StartTLS-salausta (kaikki muut portit), voi käytetäänkö salausta lainkaan.", + "ManageClients": "Hallitse palveluita", + "NoApplicationsFound": "Sovelluksia ei löytynyt", + "DownloadClientAriaSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Aria2:n oletusta jättämällä tyhjäksi.", + "UrlBaseHelpText": "Käänteisen välityspalvelimen tukea varten. Oletusarvo on tyhjä.", + "Donate": "Lahjoita", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Lisää median ominaisuuksia tunnisteina. Vihjeet ovat esimerkkejä.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä rTorrentin oletusta jättämällä tyhjäksi.", + "DownloadClientSettingsUseSslHelpText": "Muodosta {clientName}-yhteys käyttäen salattua yhteyttä.", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Vaihtoehtoinen latausten tallennussijainti. Käytä Transmissionin oletusta jättämällä tyhjäksi.", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää latauspalvelun {clientName} RPC-URL-osoitteeseen etuliitteen, esim. \"{url}\". Oletus on \"{defaultUrl}\".", + "IndexerSettingsAppsMinimumSeedersHelpText": "Sovellusten edellyttämä hakupalvelusta kaapattavien kohteiden jakajien (seed) vähimmäismäärä. Jos tyhjä, käytetään synkronointiprofiilin oletusta.", + "Menu": "Valikko", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Synkronoi estetyt torrent-hajautusarvot kaapattaessa", + "IndexerBeyondHDSettingsSearchTypes": "Etsittävät tyypit", + "IndexerSettingsSeedRatio": "Jakosuhde", + "IndexerSettingsSeedTime": "Jakoaika", + "IndexerSettingsSeedTimeHelpText": "Aika, joka torrentia tulee jakaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi.", + "IndexerSettingsVipExpiration": "VIP-erääntyy", + "Destination": "Kohde", + "Directory": "Kansio", + "DownloadClientFloodSettingsTagsHelpText": "Latauksen alkuperäiset tunnisteet, jotka tarvitaan sen tunnistamiseen. Tämä välttää ristiriidat muiden latausten kanssa.", + "DownloadClientFreeboxSettingsApiUrl": "Rajapinnan URL", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä saatu app_token-tietue.", + "DownloadClientFreeboxSettingsHostHelpText": "Freeboxin isäntänimi tai IP-osoite. Oletus on \"{url}\" (toimii vain samassa verkossa).", + "DownloadClientPneumaticSettingsStrmFolder": "Strm-kansio", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Tila, jossa torrentit lisätään qBittorrentiin. Huomioi, että pakotetut torrentit eivät noudata nopeusrajoituksia.", + "DownloadClientSettingsAddPaused": "Lisää keskeytettynä", + "DownloadClientSettingsDestinationHelpText": "Määrittää manuaalisen tallennuskohteen. Käytä oletusta jättämällä tyhjäksi.", + "DownloadClientSettingsInitialState": "Aloitustila", + "DownloadClientSettingsInitialStateHelpText": "Latauspalveluun {clientName} lisättyjen torrentien aloitustila.", + "IndexerHDBitsSettingsCodecs": "Koodekit", + "IndexerHDBitsSettingsCodecsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", + "IndexerHDBitsSettingsMediums": "Muodot", + "IndexerHDBitsSettingsMediumsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", + "IndexerHDBitsSettingsOriginsHelpText": "Jos ei määritetty, käytetään kaikkia vaihtoehtoja.", + "IndexerSettingsAdditionalParameters": "Muut parametrit", + "IndexerSettingsApiPath": "Rajapinnan sijainti", + "IndexerSettingsApiPathHelpText": "Rajapinnan sijainti, yleensä \"{url}\".", + "IndexerSettingsCookie": "Eväste", + "IndexerSettingsPackSeedTime": "Paketin jakoaika", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Aika, joka koostepaketin (kuten sarjan tuotantokauden tai esittäjän diskografian) sisältävää torrentia tulee jakaa. Käytä sovelluksen oletusta jättämällä tyhjäksi.", + "IndexerSettingsSeedRatioHelpText": "Suhde, joka torrentin tulee saavuttaa ennen sen pysäytystä. Käytä latauspalvelun oletusta jättämällä tyhjäksi. Suhteen tulisi olla ainakin 1.0 ja noudattaa hakupalvelun sääntöjä.", + "SecretToken": "Salainen tunniste", + "TorrentBlackholeSaveMagnetFiles": "Tallenna magnet-tiedostot", + "UseSsl": "Käytä SSL-salausta", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Jos torrent on estetty hajautusarvon perusteella sitä ei välttämättä hylätä oikein joidenkin hakupalveluiden RSS-syötteestä tai hausta. Tämän käyttöönotto mahdollistaa tällaisten torrentien hylkäämisen kaappauksen jälkeen, kuitenkin ennen kuin niitä välitetään latauspalvelulle.", + "BlackholeFolderHelpText": "Kansio, jonne {appName} tallentaa {extension}-tiedoston.", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Lisää Delugen JSON-URL-osoitteeseen etuliitteen, ks. \"{url})\".", + "DownloadClientFloodSettingsAdditionalTags": "Lisätunnisteet", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Tämän kansion .strm-tiedostot tuodaan droonilla.", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Lataa tiedostot järjestyksessä (qBittorrent 4.1.0+).", + "UsenetBlackholeNzbFolder": "NZB-kansio", + "XmlRpcPath": "XML RPC -sijainti", + "DownloadClientSettingsUrlBaseHelpText": "Lisää lataustuökalun {clientName} URL-osoitteeseen etuliitteen, esim. \"{url}\".", + "DownloadClientFloodSettingsUrlBaseHelpText": "Lisää Flood-rajapintaan etuliitteen, esim. \"{url}\".", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Vaihtoehtoinen jaettu kansio latauksille. Käytä Download Stationin oletussijaintia jättämällä tyhjäksi.", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Määritä Freebox-rajapinnan perus-URL rajapinnan versiolla, esim. \"{url}\". Oletus on \"{defaultApiUrl}\".", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Ensimmäinen ja viimeinen ensin", + "DownloadClientFreeboxSettingsAppId": "Sovelluksen ID", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox-liittymän portti. Oletus on {port}.", + "DownloadClientPneumaticSettingsNzbFolder": "NZB-kansio", + "DownloadClientQbittorrentSettingsSequentialOrder": "Peräkkäinen järjestys", + "CustomFilter": "Oma suodatin", + "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox-rajapinnan käyttöoikeutta määritettäessä käytettävä App ID -sovellustunniste.", + "DownloadClientFreeboxSettingsAppToken": "Sovellustietue", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGetin version 16.0.", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Tämän kansion on oltava Kodin tavoitettavissa.", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Lataa ensimmäinen ja viimeinen osa ensin (qBittorrent 4.1.0+).", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Käytä suojattua yhteyttä. Katso qBittorentin asetusten \"Selainkäyttö\"-osion \"Käytä HTTPS:ää HTTP:n sijaan\" -asetus.", + "DownloadClientRTorrentSettingsAddStopped": "Lisää pysäytettynä", + "DownloadClientRTorrentSettingsUrlPath": "URL-sijainti", + "TorrentBlackholeSaveMagnetFilesHelpText": "Tallenna magnet-linkki, jos .torrent-tiedostoa ei ole käytettävissä (hyödyllinen vain latauspalvelun tukiessa tiedostoon tallennettuja magnet-linkkejä).", + "TorrentBlackholeTorrentFolder": "Torrent-kansio", + "TorrentBlackholeSaveMagnetFilesExtension": "Tallenna magnet-tiedostojen pääte", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".", + "LabelIsRequired": "Nimi on pakollinen", + "Default": "Oletus", + "GrabRelease": "Kaappaa julkaisu", + "OverrideGrabModalTitle": "Ohitetaan ja kaapataan – {title}", + "PrioritySettings": "Painotus: {priority}", + "SelectDownloadClientModalTitle": "{modalTitle} – Valitse latauspalvelu", + "ProxyValidationBadRequest": "Välityspalvelintesti epäonnistui. Tilakoodi: {statusCode}.", + "ProxyValidationUnableToConnect": "Välityspalvelinta ei tavoiteta: {exceptionMessage}. Saat lisätietoja virheen lähellä olevista lokimerkinnöistä.", + "ManualGrab": "Manuaalikaappaus", + "OverrideAndAddToDownloadClient": "Ohita ja lisää etälatauspalveluun", + "BuiltIn": "Sisäänrakennettu", + "Any": "Mikä tahansa", + "Script": "Komentosarja", + "InfoUrl": "Tietojen URL", + "PublishedDate": "Julkaisupäivä", + "Redirected": "Uudelleenohjattu", + "AllSearchResultsHiddenByFilter": "Aktiivinen suodatin piilottaa kaikki tulokset.", + "HealthMessagesInfoBox": "Saat lisätietoja näiden vakausviestien syistä painamalla rivin lopussa olevaa wikilinkkiä (kirjakuvake) tai tarkastelemalla [lokitietoja]({link}). Mikäli et osaa tulkita näitä viestejä, tavoitat tukemme alla olevilla linkeillä.", + "PackageVersionInfo": "{packageVersion} julkaisijalta {packageAuthor}", + "ErrorRestoringBackup": "Virhe palautettaessa varmuuskopiota", + "ExternalUpdater": "{appName} on määritetty käyttämään ulkoista päivitysratkaisua.", + "FailedToFetchUpdates": "Päivitysten nouto epäonnistui", + "AptUpdater": "Asenna päivitys APT-työkalun avulla", + "DockerUpdater": "Hanki päivitys päivittämällä Docker-säiliö", + "Download": "Lataa", + "LogFilesLocation": "Lokitiedostojen tallennussijainti: {location}", + "Logout": "Kirjaudu ulos", + "NoEventsFound": "Tapahtumia ei löytynyt", + "RestartReloadNote": "Huomioi: {appName} käynnistyy palautusprosessin aikana automaattisesti uudelleen.", + "TheLogLevelDefault": "Lokikirjauksen oletusarvoinen laajuus on \"Informatiivinen\". Laajuutta voidaan muuttaa [Yleisistä asetuksista](/settings/general).", + "UpdateAppDirectlyLoadError": "{appName}ia ei voida päivittää suoraan,", + "UpdaterLogFiles": "Päivittäjän lokitiedostot", + "WouldYouLikeToRestoreBackup": "Haluatko palauttaa varmuuskopion \"{name}\"?", + "InstallLatest": "Asenna uusin", + "CurrentlyInstalled": "Käytössä oleva versio", + "PreviouslyInstalled": "Aiemmin käytössä ollut versio", + "Mixed": "Sekoitettu", + "IndexerSettingsAppsMinimumSeeders": "Jakajien vähimmäismäärä", + "FailedToFetchSettings": "Asetusten nouto epäonnistui", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Älä huomioi tulosten SCENE-julkaisuja.", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Polku XMLRPC-päätteeseen, ks. \"{url}\". Käytettäessä ruTorrentia tämä on yleensä RPC2 tai [ruTorrentin sijainti]{url2}.", + "InstallMajorVersionUpdateMessageLink": "Saat lisätietoja osoitteesta [{domain}]({url}).", + "Install": "Asenna", + "NotificationsTelegramSettingsIncludeAppName": "Sisällytä {appName} otsikkoon", + "InstallMajorVersionUpdate": "Asenna päivitys", + "InstallMajorVersionUpdateMessage": "Tämä päivitys asentaa uuden pääversion, joka ei välttämättä ole yhteensopiva laitteistosi kanssa. Haluatko varmasti asentaa päivityksen?", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Ilmoitukset voidaan tarvittaessa erottaa muista sovelluksista lisäämällä niiden eteen \"{appName}\".", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Tämä lisää torrentit ja magnet-linkit rTorentiin pysäytetyssä tilassa. Tämä saattaa rikkoa margnet-tiedostot.", + "DownloadClientSettingsPriorityItemHelpText": "Kaapatuille kohteille käytettävä painotus.", + "LogSizeLimit": "Lokin kokorajoitus", + "LogSizeLimitHelpText": "Lokitiedoston enimmäiskoko ennen pakkausta. Oletusarvo on 1 Mt.", + "IndexerAlphaRatioSettingsExcludeScene": "Ohita SCENE-julkaisut", + "AverageGrabs": "Kaappausten keskiarvo", + "AverageQueries": "Kyselyiden keskiarvo", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Muut Newznab-parametrit", + "IndexerNewznabSettingsVipExpirationHelpText": "Syötä VIP-tilan päättymispäivä (yyyy-mm-dd) tai jätä tyhjäksi. {appName} ilmoittaa viikko ennen VIP-tilan päättymistä.", + "IndexerSettingsQueryLimitHelpText": "Kyselyiden enimmäismäärä, jonka {appName} sallii hakupalvelulle.", + "IndexerHDBitsSettingsPasskeyHelpText": "Pääsyavain käyttäjätiedoista", + "DefaultCategory": "Oletuskategoria", + "ClickToChangeQueryOptions": "Muuta kyselyasetuksia painamalla tästä.", + "IndexerSettingsQueryLimit": "Kyselyrajoitus", + "IndexerAvistazSettingsPidHelpText": "PID \"My Account\" tai \"My Profile\" -sivulta.", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Vain vähintään jäsen-tasoinen käyttäjä voi käyttää tämän hakupalvelun rajapintaa.", + "IndexerBeyondHDSettingsLimitedOnly": "Vain rajoitetut", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Etsi vain Freeleech-julkaisuja.", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Avaimella on oltava \"User\" ja \"Torrents\" oikeudet.", + "IndexerHDBitsSettingsUseFilenames": "Käytä tiedostonimiä", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerIPTorrentsSettingsCookieUserAgent": "Evästeen käyttäjäagentti", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Evästeeseen liitetty selaimen käyttäjäagentti.", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Sivuston rajapinnan avain", + "IndexerOrpheusSettingsApiKeyHelpText": "Sivuston rajapinnan avain. Löytyy kohdasta \"Settings\" > \"Access Settings\".", + "Open": "Avaa", + "PreferMagnetUrl": "Suosi magnet-URL:eja", + "ProwlarrDownloadClientsAlert": "Latauspalvelut on määritettävä tässä vain, jos hakuja aiotaan tehdä suoraan {appName}ista. Sovelluksien hauille käytetään niiden omien asetusten latauspalvelumäärityksiä.", + "IndexerSettingsLimitsUnitHelpText": "Hakupalveluiden aikarajoituksiin käytettävä yksikkö.", + "IndexerGazelleGamesSettingsSearchGroupNames": "Etsi ryhmien nimillä", + "DownloadClientSettingsDefaultCategoryHelpText": "Oletusarvoinen varakategoria julkaisuille, joilla ei ole kategoriaa. {appName}-kategorian lisääminen välttää ristiriidat ei-{appName} latausten kanssa. Kategoria on valinnainen, mutta erittäin suositeltava.", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "PreferMagnetUrlHelpText": "Hakupalvelu käyttää kaappaukseen ensisijaisesti magnet-linkkejä ja varmistuksena torrent-linkkejä.", + "ProwlarrDownloadClientsInAppOnlyAlert": "Latauspalvelut ovat vain {appName}in sisäisiä hakuja varten, eikä niitä synkronoida muihin sovelluksiin (ominaisuutta ole myöskään suunnitteilla).", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Oletusarvoinen varakategoria julkaisuille, joilla ei ole kategoriaa. {appName}-kategorian lisääminen välttää ristiriidat ei-{appName} latausten kanssa. Kategoria on valinnainen, mutta erittäin suositeltava. Luo kohdekansioon [kategoria]-alikansion.", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerAvistazSettingsUsernameHelpText": "Sivuston käyttäjätunnus", + "IndexerBeyondHDSettingsRefundOnly": "Vain \"Refund\"", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Etsi vain Refund-julkaisuja.", + "IndexerBeyondHDSettingsRewindOnly": "Vain \"Rewind\"", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Etsi vain Rewind-julkaisuja.", + "IndexerBeyondHDSettingsApiKeyHelpText": "Sivuston rajapinnan avain. Löytyy kohdasta \"My Security\" > \"API Key\".", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Valitse halutut julkaisutyypit. Jos mitään ei ole valittu käytetään kaikkia.", + "IndexerFileListSettingsPasskeyHelpText": "Sivuston pääsyavain. Tämä on latauspalvelussasi näkyvä trakkerin URL-osoitteen aakkosnumeerinen osa.", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Sivuston rajapinnan avain. Löytyy kohdasta \"Settings\" > \"Access Settings\".", + "IndexerFileListSettingsUsernameHelpText": "Sivuston käyttäjätunnus", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Etsi julkaisuja ryhmien nimillä.", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Näytä vain Freeleech-julkaisut.", + "IndexerNzbIndexSettingsApiKeyHelpText": "Sivuston rajapinnan avain", + "IndexerHDBitsSettingsOrigins": "Alkuperä", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Käsittele torrentien tiedostonimiä julkaisujen nimikkeinä.", + "IndexerHDBitsSettingsUsernameHelpText": "Sivuston käyttäjätunnus", + "IndexerSettingsGrabLimitHelpText": "Kaappausten enimmäismäärä, jonka {appName} sallii hakupalvelulle.", + "IndexerRedactedSettingsApiKeyHelpText": "Sivuston rajapinnan avain. Löytyy kohdasta \"Settings\" > \"Access Settings\".", + "IndexerSettingsApiUser": "Rajapinnan käyttäjä", + "IndexerSettingsBaseUrl": "Perus-URL", + "IndexerSettingsBaseUrlHelpText": "Määritä verkkotunnus, jota {appName} käyttää sivustolle lähetettäville pyynnöille.", + "IndexerPassThePopcornSettingsApiUserHelpText": "Nämä löytyvät PassThePopcorn-tilin asetuksista, kohdasta \"Edit Profile\" > \"Security\".", + "IndexerSettingsPasskey": "Pääsyavain", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Etsi vain Freeleech-julkaisuja (rajoitettu UL).", + "IndexerBeyondHDSettingsRssKeyHelpText": "Sivuston RSS-avain. Löytyy kohdasta \"My Security\" > \"RSS-avain\".", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Etsi vain freeleech-julkaisuja.", + "IndexerNewznabSettingsApiKeyHelpText": "Sivuston rajapinnan avain", + "IndexerNebulanceSettingsApiKeyHelpText": "Rajapinnan avain kohdasta \"Settings\" > \"Api Keys\".\nAvaimella on oltava \"List\" ja \"Download\" oikeudet.", + "IndexerSettingsGrabLimit": "Kaappausrajoitus", + "IndexerSettingsLimitsUnit": "Rajoitusten yksikkö", + "IndexerSettingsRssKey": "RSS-avain", + "IndexerMTeamTpSettingsApiKeyHelpText": "Sivuston rajapinnan avain. Löytyy kohdasta \"Control Panel\" > \"Security\" > \"Laboratory\".", + "IndexerSettingsPreferMagnetUrlHelpText": "Hakupalvelu käyttää kaappaukseen ensisijaisesti magnet-linkkejä ja varmistuksena torrent-linkkejä.", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Sivuston rajapinnan avain", + "IndexerSettingsPreferMagnetUrl": "Suosi magnet-URL:eja", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Vain \"Golden Popcorn\"", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Etsi vain ns. kultaisella pocornilla merkittyjä julkaisuja.", + "IndexerSettingsFreeleechOnly": "Vain \"Freeleech\"", + "IndexerSettingsCookieHelpText": "Jos sivusto vaatii kirjautumisevästeen, on se noudettava selaimen avulla.", + "IndexerAvistazSettingsPasswordHelpText": "Sivuston salasana" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 306cf05a0..8768f9c50 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1,9 +1,9 @@ { - "IndexerStatusCheckAllClientMessage": "Tous les indexeurs sont indisponibles en raison d'échecs", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison d'échecs", "Indexers": "Indexeurs", "Host": "Hôte", "History": "Historique", - "HideAdvanced": "Masquer param. av.", + "HideAdvanced": "Masquer les Options Avancées", "Health": "Santé", "General": "Général", "Folder": "Dossier", @@ -11,7 +11,7 @@ "Files": "Fichiers", "Events": "Événements", "Edit": "Modifier", - "DownloadClientStatusCheckAllClientMessage": "Aucun client de téléchargement n'est disponible en raison d'échecs", + "DownloadClientStatusAllClientHealthCheckMessage": "Aucun client de téléchargement n'est disponible en raison d'échecs", "DownloadClients": "Clients de télécharg.", "Dates": "Dates", "Date": "Date", @@ -26,16 +26,16 @@ "Analytics": "Statistiques", "All": "Tout", "About": "À propos", - "IndexerStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison d'échecs : {0}", - "DownloadClientStatusCheckSingleClientMessage": "Clients de Téléchargement indisponibles en raison d'échecs : {0}", - "SetTags": "Définir des balises", + "IndexerStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs : {indexerNames}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de Téléchargement indisponibles en raison d'échecs : {downloadClientNames}", + "SetTags": "Définir des étiquettes", "ReleaseStatus": "Statut de la version", - "UpdateCheckUINotWritableMessage": "Impossible d'installer la mise à jour car le dossier d'interface utilisateur '{0}' n'est pas accessible en écriture par l'utilisateur '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Impossible d'installer la mise à jour car le dossier de démarrage '{0}' se trouve dans un dossier App Translocation.", - "UpdateCheckStartupNotWritableMessage": "Impossible d'installer la mise à jour car le dossier de démarrage '{0}' n'est pas accessible en écriture par l'utilisateur '{1}'.", + "UpdateUiNotWritableHealthCheckMessage": "Impossible d'installer la mise à jour car le dossier d'interface utilisateur '{uiFolder}' n'est pas accessible en écriture par l'utilisateur '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Impossible d'installer la mise à jour car le dossier de démarrage '{startupFolder}' se trouve dans un dossier App Translocation.", + "UpdateStartupNotWritableHealthCheckMessage": "Impossible d'installer la mise à jour car le dossier de démarrage '{startupFolder}' n'est pas accessible en écriture par l'utilisateur '{userName}'.", "UnselectAll": "Tout désélectionner", "UISettingsSummary": "Date, langue, et perceptions des couleurs", - "TagsSettingsSummary": "Voir toutes les balises et comment elles sont utilisées. Les balises inutilisées peuvent être supprimées", + "TagsSettingsSummary": "Voir toutes les étiquettes et comment elles sont utilisées. Les étiquettes inutilisées peuvent être supprimées", "Style": "Style", "Status": "État", "Sort": "Trier", @@ -51,13 +51,13 @@ "ReleaseBranchCheckOfficialBranchMessage": "La branche {0} n'est pas une branche de version {appName} valide, vous ne recevrez pas de mises à jour", "Refresh": "Rafraîchir", "Queue": "File d'attente", - "ProxyCheckResolveIpMessage": "Impossible de résoudre l'adresse IP de l'hôte proxy configuré {0}", - "ProxyCheckFailedToTestMessage": "Échec du test du proxy : {0}", - "ProxyCheckBadRequestMessage": "Échec du test du proxy. Code d'état : {0}", + "ProxyResolveIpHealthCheckMessage": "Impossible de résoudre l'adresse IP de l'hôte proxy configuré {proxyHostName}", + "ProxyFailedToTestHealthCheckMessage": "Échec du test du proxy : {url}", + "ProxyBadRequestHealthCheckMessage": "Échec du test du proxy. Code d'état : {statusCode}", "Proxy": "Proxy", "Protocol": "Protocole", "Options": "Options", - "NoChanges": "Aucuns changements", + "NoChanges": "Aucun changement", "NoChange": "Pas de changement", "MoreInfo": "Plus d'informations", "Grabbed": "Saisie", @@ -107,17 +107,17 @@ "Reload": "Recharger", "Peers": "Peers", "PageSize": "Pagination", - "Ok": "Ok", + "Ok": "OK", "OAuthPopupMessage": "Les pop-ups sont bloquées par votre navigateur", "Name": "Nom", "Message": "Message", "Level": "Niveau", "KeyboardShortcuts": "Raccourcis clavier", - "HealthNoIssues": "Aucun problème avec votre configuration", - "SystemTimeCheckMessage": "L'heure du système est décalée de plus d'un jour. Les tâches planifiées peuvent ne pas s'exécuter correctement tant que l'heure ne sera pas corrigée", + "NoIssuesWithYourConfiguration": "Aucun problème avec votre configuration", + "SystemTimeHealthCheckMessage": "L'heure du système est décalée de plus d'un jour. Les tâches planifiées peuvent ne pas s'exécuter correctement tant que l'heure ne sera pas corrigée", "SettingsShowRelativeDates": "Afficher les dates relatives", "UnsavedChanges": "Modifications non enregistrées", - "ShowSearchHelpText": "Afficher le bouton de recherche au survol", + "ShowSearchHelpText": "Affiche le bouton de recherche au survol", "ShowSearch": "Afficher la recherche", "SettingsTimeFormat": "Format de l'heure", "SettingsShowRelativeDatesHelpText": "Afficher les dates relatives (aujourd'hui, hier, etc.) ou absolues", @@ -146,7 +146,7 @@ "ApplyTags": "Appliquer les étiquettes", "AppDataDirectory": "Dossier AppData", "ApiKey": "Clé API", - "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs vers les serveurs de {appName}. Cela inclut des informations sur votre navigateur, quelle page {appName} WebUI vous utilisez, les rapports d'erreurs, ainsi que le système d'exploitation et sa version. Nous utiliserons ces informations pour prioriser les nouvelles fonctionnalités et les corrections de bugs.", + "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs aux serveurs de {appName}. Cela inclut des informations sur votre navigateur, les pages de l'interface Web de {appName} que vous utilisez, les rapports d'erreurs ainsi que le système d'exploitation et la version d'exécution. Nous utiliserons ces informations pour prioriser les fonctionnalités et les corrections de bugs.", "IgnoredAddresses": "Adresses ignorées", "Hostname": "Nom d'hôte", "GeneralSettings": "Réglages généraux", @@ -158,17 +158,17 @@ "Enable": "Activer", "DownloadClientSettings": "Télécharger les paramètres client", "Docker": "Docker", - "DeleteTag": "Supprimer le tag", + "DeleteTag": "Supprimer l'étiquette", "DeleteNotification": "Supprimer la notification", "DeleteDownloadClient": "Supprimer le client de téléchargement", "DeleteBackup": "Supprimer la sauvegarde", - "DBMigration": "Migration de la base de données", + "DatabaseMigration": "Migration des bases de données", "ConnectSettings": "Paramètres de connexion", - "BackupFolderHelpText": "Les chemins correspondants seront sous le répertoire AppData de {appName}", + "BackupFolderHelpText": "Les chemins d'accès relatifs se trouvent dans le répertoire AppData de {appName}", "IllRestartLater": "Je redémarrerai plus tard", "CancelPendingTask": "Êtes-vous sur de vouloir annuler cette tâche en attente ?", "BranchUpdateMechanism": "Branche utilisée par le mécanisme de mise à jour extérieur", - "BranchUpdate": "Branche à utiliser pour mettre {appName} à jour", + "BranchUpdate": "Branche à utiliser pour mettre à jour {appName}", "BeforeUpdate": "Avant la mise à jour", "DeleteDownloadClientMessageText": "Voulez-vous supprimer le client de téléchargement « {name} » ?", "DeleteBackupMessageText": "Voulez-vous supprimer la sauvegarde « {name} » ?", @@ -207,14 +207,14 @@ "ProxyBypassFilterHelpText": "Utilisez ',' comme séparateur et '*.' comme caractère générique pour les sous-domaines", "Uptime": "Disponibilité", "UpdateScriptPathHelpText": "Chemin d'accès à un script personnalisé qui prend un package de mise à jour extrait et gère le reste du processus de mise à jour", - "UpdateMechanismHelpText": "Utiliser le programme de mise à jour intégré de {appName} ou un script", + "UpdateMechanismHelpText": "Utilisez le programme de mise à jour intégré de {appName} ou un script", "UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour", "UnableToLoadUISettings": "Impossible de charger les paramètres de l'interface utilisateur", "UnableToLoadTags": "Impossible de charger les étiquettes", "UnableToLoadHistory": "Impossible de charger l'historique", "UnableToLoadGeneralSettings": "Impossible de charger les paramètres généraux", - "UnableToLoadDownloadClients": "Impossible de charger les clients de téléchargement", - "UnableToLoadBackups": "Impossible de charger les sauvegardes", + "DownloadClientsLoadError": "Impossible de charger les clients de téléchargement", + "BackupsLoadError": "Impossible de charger les sauvegardes", "UnableToAddANewNotificationPleaseTryAgain": "Impossible d'ajouter une nouvelle notification, veuillez réessayer.", "UnableToAddANewIndexerPleaseTryAgain": "Impossible d'ajouter un nouvel indexeur, veuillez réessayer.", "UnableToAddANewDownloadClientPleaseTryAgain": "Impossible d'ajouter un nouveau client de téléchargement, veuillez réessayer.", @@ -222,8 +222,8 @@ "TagsHelpText": "S'applique aux indexeurs avec au moins une étiquette correspondante", "StartTypingOrSelectAPathBelow": "Commencer à écrire ou sélectionner un chemin ci-dessous", "NoTagsHaveBeenAddedYet": "Aucune identification n'a été ajoutée pour l'instant", - "IndexerFlags": "Indicateurs d'indexeur", - "DeleteTagMessageText": "Voulez-vous vraiment supprimer l'étiquette « {label} » ?", + "IndexerFlags": "Drapeaux de l'indexeur", + "DeleteTagMessageText": "Voulez-vous vraiment supprimer l'étiquette '{label}' ?", "UISettings": "Paramètres UI", "UILanguageHelpTextWarning": "Rechargement du navigateur requis", "UILanguageHelpText": "Langue que {appName} utilisera pour l'interface utilisateur", @@ -241,7 +241,7 @@ "SendAnonymousUsageData": "Envoyer des données d'utilisation anonymes", "ScriptPath": "Chemin du script", "SaveSettings": "Enregistrer les paramètres", - "RSSIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur", + "RssIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur", "Retention": "Rétention", "Result": "Résultat", "Restore": "Restaurer", @@ -282,10 +282,10 @@ "CloseCurrentModal": "Fermer cette fenêtre modale", "AddingTag": "Ajout d'une étiquette", "OnHealthIssueHelpText": "Sur un problème de santé", - "AcceptConfirmationModal": "Accepter les modalités d'utilisations", + "AcceptConfirmationModal": "Accepter les modalités d'utilisation", "OpenThisModal": "Ouvrir cette fenêtre modale", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison de pannes pendant plus de 6 heures : {0}", - "IndexerLongTermStatusCheckAllClientMessage": "Tous les indexeurs sont indisponibles en raison d'échecs de plus de 6 heures", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison de pannes pendant plus de 6 heures : {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison d'échecs de plus de 6 heures", "Yesterday": "Hier", "Tomorrow": "Demain", "Today": "Aujourd'hui", @@ -302,7 +302,7 @@ "SettingsFilterSentryEvents": "Filtrer les événements d'analyse", "SettingsConsoleLogLevel": "Niveau de journalisation de la console", "SearchIndexers": "Recherche indexeurs", - "IndexerRss": "Indexeur Rss", + "IndexerRss": "Indexeur RSS", "IndexerQuery": "Requête indexeur", "IndexerObsoleteCheckMessage": "Les indexeurs sont obsolètes ou ont été mis à jour : {0}. Veuillez supprimer et (ou) rajouter à {appName}", "IndexerHealthCheckNoIndexers": "Aucun indexeur activé, {appName} ne renverra pas de résultats de recherche", @@ -322,7 +322,7 @@ "AddedToDownloadClient": "Release ajoutée au client", "AddNewIndexer": "Ajouter un nouvel indexeur", "Wiki": "Wiki", - "RSS": "RSS", + "Rss": "RSS", "RedirectHelpText": "Rediriger la demande de téléchargement entrante pour l'indexeur et transmettre la capture directement au lieu de transmettre la demande par proxy via {appName}", "Redirect": "Rediriger", "Reddit": "Reddit", @@ -339,13 +339,13 @@ "Apps": "Applications", "Auth": "Auth", "Category": "Catégorie", - "Custom": "Customisé", + "Custom": "Personnaliser", "DeleteAppProfile": "Supprimer le profil de l'application", "Description": "Description", "Donations": "Dons", "Enabled": "Activé", "Grabs": "Complétés", - "Id": "Id", + "Id": "ID", "Presets": "Préconfigurations", "Privacy": "Visibilité", "Query": "Requête", @@ -368,13 +368,13 @@ "AppSettingsSummary": "Applications et paramètres pour configurer comment {appName} interagit avec vos programmes PVR", "IndexerTagsHelpText": "Utilisez des étiquettes pour spécifier les proxies d'indexation ou les applications avec lesquelles l'indexeur est synchronisé.", "Notifications": "Notifications", - "IndexerVipCheckExpiredClientMessage": "Les avantages VIP de l'indexeur ont expiré : {0}", + "IndexerVipExpiredHealthCheckMessage": "Les avantages VIP de l'indexeur ont expiré : {indexerNames}", "IndexerProxy": "Proxy d'indexation", "IndexerSettingsSummary": "Configuration de divers paramètres globaux de l'indexeur, y compris les proxies.", "IndexerProxies": "Proxys d'indexation", - "IndexerProxyStatusCheckAllClientMessage": "Tous les proxys sont indisponibles en raison d'échecs", - "IndexerProxyStatusCheckSingleClientMessage": "Proxys indisponibles en raison d'échecs : {0}", - "IndexerVipCheckExpiringClientMessage": "Les avantages VIP de l'indexeur arrivent bientôt à expiration : {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tous les proxys sont indisponibles en raison d'échecs", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxys indisponibles en raison d'échecs : {indexerProxyNames}", + "IndexerVipExpiringHealthCheckMessage": "Les avantages VIP de l'indexeur arrivent bientôt à expiration : {indexerNames}", "NoLinks": "Aucun liens", "Notification": "Notification", "UnableToAddANewIndexerProxyPleaseTryAgain": "Impossible d'ajouter un nouveau proxy d'indexation, veuillez réessayer.", @@ -383,8 +383,8 @@ "HistoryCleanupDaysHelpText": "Définir sur 0 pour désactiver le nettoyage automatique", "HistoryCleanupDaysHelpTextWarning": "Les fichiers dans la corbeille plus anciens que le nombre de jours sélectionné seront nettoyés automatiquement", "OnGrab": "Récupéré à la sortie", - "OnHealthIssue": "Sur la question de la santé", - "TestAllIndexers": "Testez tous les indexeurs", + "OnHealthIssue": "Lors de problème de santé", + "TestAllIndexers": "Tester tous les indexeurs", "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent fourni par l'application qui a appelé l'API", "Database": "Base de données", "HistoryCleanup": "Nettoyage de l'historique", @@ -400,13 +400,13 @@ "SearchType": "Type de recherche", "Categories": "Catégories", "MassEditor": "Éditer en masse", - "UnableToLoadApplicationList": "Impossible de charger la liste des applications", + "ApplicationsLoadError": "Impossible de charger la liste des applications", "Website": "Site internet", "AudioSearch": "Recherche de musique", "BookSearch": "Recherche de livres", - "OnApplicationUpdate": "Sur la mise à jour de l'application", - "OnApplicationUpdateHelpText": "Lors de la mise à jour de l'app", - "IndexerNoDefCheckMessage": "Les indexeurs ne sont pas définis et ne fonctionneront pas :Merci de les retirer et (ou) les ajouter à nouveau à {appName}", + "OnApplicationUpdate": "Lors de la mise à jour de l'application", + "OnApplicationUpdateHelpText": "Lors de la mise à jour de l'application", + "IndexerNoDefinitionCheckHealthCheckMessage": "Les indexeurs ne sont pas définis et ne fonctionneront pas : {indexerNames}. Merci de les retirer et (ou) les ajouter à nouveau à {appName}.", "MovieSearch": "Recherche de films", "TvSearch": "Recherche de séries TV", "Application": "Applications", @@ -458,7 +458,7 @@ "AuthenticationRequiredWarning": "Pour empêcher l'accès à distance sans authentification, {appName} exige désormais que l'authentification soit activée. Vous pouvez éventuellement désactiver l'authentification pour les adresses locales.", "Remove": "Retirer", "Replace": "Remplacer", - "TheLatestVersionIsAlreadyInstalled": "La dernière version de {appName} est déjà installée", + "OnLatestVersion": "La dernière version de {appName} est déjà installée", "AddCustomFilter": "Ajouter filtre personnalisé", "AddApplication": "Ajouter une application", "IncludeManualGrabsHelpText": "Inclure les saisies manuelles effectuées dans {appName}", @@ -482,7 +482,7 @@ "Author": "Auteur", "AverageResponseTimesMs": "Temps de réponse moyen des indexeurs (ms)", "IndexerFailureRate": "Taux d'échec de l'indexeur", - "Label": "Label", + "Label": "Étiquette", "More": "Plus", "Publisher": "Éditeur", "Season": "Saison", @@ -493,7 +493,7 @@ "Track": "Piste", "Year": "Année", "ApplicationURL": "URL de l'application", - "ApiKeyValidationHealthCheckMessage": "Veuillez mettre à jour votre clé API pour qu'elle contienne au moins {0} caractères. Vous pouvez le faire via les paramètres ou le fichier de configuration", + "ApiKeyValidationHealthCheckMessage": "Veuillez mettre à jour votre clé API pour qu'elle contienne au moins {length} caractères. Vous pouvez le faire via les paramètres ou le fichier de configuration", "ApplicationUrlHelpText": "L'URL externe de cette application, y compris http(s)://, le port ainsi que la base de URL", "ApplyChanges": "Appliquer les modifications", "ApplyTagsHelpTextAdd": "Ajouter : ajoute les étiquettes à la liste de étiquettes existantes", @@ -509,7 +509,7 @@ "DeleteSelectedDownloadClients": "Supprimer le(s) client(s) de téléchargement", "DeleteSelectedDownloadClientsMessageText": "Voulez-vous vraiment supprimer {count} client(s) de téléchargement sélectionné(s) ?", "StopSelecting": "Effacer la sélection", - "UpdateAvailable": "Une nouvelle mise à jour est disponible", + "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible : {version}", "AdvancedSettingsHiddenClickToShow": "Paramètres avancés masqués, cliquez pour afficher", "AdvancedSettingsShownClickToHide": "Paramètres avancés affichés, cliquez pour masquer", "AppsMinimumSeeders": "Apps avec le nombre minimum de seeders disponibles", @@ -528,7 +528,7 @@ "ConnectionLostReconnect": "{appName} essaiera de se connecter automatiquement, ou vous pouvez cliquer sur « Recharger » en bas.", "ConnectionLostToBackend": "{appName} a perdu sa connexion au backend et devra être rechargé pour fonctionner à nouveau.", "RecentChanges": "Changements récents", - "WhatsNew": "Quoi de neuf ?", + "WhatsNew": "Quoi de neuf ?", "minutes": "minutes", "DeleteAppProfileMessageText": "Voulez-vous vraiment supprimer le profil d'application « {name} » ?", "AddConnection": "Ajouter une connexion", @@ -537,7 +537,7 @@ "AddIndexerImplementation": "Ajouter un indexeur - {implementationName}", "EditConnectionImplementation": "Modifier la connexion - {implementationName}", "NotificationStatusAllClientHealthCheckMessage": "Toutes les notifications ne sont pas disponibles en raison d'échecs", - "NotificationStatusSingleClientHealthCheckMessage": "Notifications indisponibles en raison d'échecs : {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notifications indisponibles en raison d'échecs : {notificationNames}", "EditApplicationImplementation": "Ajouter une condition - {implementationName}", "EditIndexerImplementation": "Modifier l'indexeur - {implementationName}", "EditIndexerProxyImplementation": "Modifier un proxy d'indexeur - {implementationName}", @@ -587,19 +587,225 @@ "AddDownloadClientImplementation": "Ajouter un client de téléchargement - {implementationName}", "ManageDownloadClients": "Gérer les clients de téléchargement", "AuthenticationRequiredPasswordHelpTextWarning": "Saisir un nouveau mot de passe", - "IndexerDownloadClientHealthCheckMessage": "Indexeurs avec des clients de téléchargement invalides : {0].", + "IndexerDownloadClientHealthCheckMessage": "Indexeurs avec des clients de téléchargement invalides : {indexerNames}.", "AuthenticationMethod": "Méthode d'authentification", "AuthenticationMethodHelpTextWarning": "Veuillez choisir une méthode d'authentification valide", "ActiveIndexers": "Indexeurs actifs", "ActiveApps": "Applications actives", "AuthenticationRequiredUsernameHelpTextWarning": "Saisir un nouveau nom d'utilisateur", - "Clone": "Cloner", + "Clone": "Dupliquer", "PackSeedTime": "Temps de Seed", - "ApplicationTagsHelpText": "Synchroniser les indexeurs avec cette application qui n'ont aucune balise ou qui ont une ou plusieurs balises correspondantes", + "ApplicationTagsHelpText": "Synchroniser les indexeurs avec cette application qui n'ont aucune étiquette ou qui ont une ou plusieurs étiquettes correspondantes. Si aucune étiquette n'est listée ici, aucun indexeur ne sera empêché de se synchroniser en raison de ses étiquettes.", "OnHealthRestored": "Sur la santé restaurée", "OnHealthRestoredHelpText": "Sur la santé restaurée", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmer le nouveau mot de passe", "PasswordConfirmation": "Confirmation du mot de passe", "NoIndexerCategories": "Aucune catégorie disponible pour cet indexeur", - "InvalidUILanguage": "Votre interface utilisateur est définie sur une langue non valide, corrigez-la et enregistrez vos paramètres" + "InvalidUILanguage": "Votre interface utilisateur est définie sur une langue non valide, corrigez-la et enregistrez vos paramètres", + "DownloadClientQbittorrentSettingsContentLayout": "Disposition du contenu", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si il faut utiliser de la présentation du contenu configurée par qBittorrent, la présentation originale du torrent ou la création systématique d'un sous-dossier (qBittorrent 4.3.2+)", + "DownloadClientAriaSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement par défaut Aria2", + "ManageClients": "Gérer les clients", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL json du déluge, voir {url}", + "Destination": "Cible", + "Directory": "Dossier", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Dossier partagé dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement par défaut de Download Station", + "CustomFilter": "Filtre personnalisé", + "NotificationsEmailSettingsUseEncryption": "Utiliser le cryptage", + "NotificationsEmailSettingsUseEncryptionHelpText": "Préférer utiliser le cryptage s'il est configuré sur le serveur, toujours utiliser le cryptage via SSL (port 465 uniquement) ou StartTLS (tout autre port) ou ne jamais utiliser le cryptage", + "IndexerHDBitsSettingsPasskeyHelpText": "Clé d'accès à partir des détails de l'utilisateur", + "NoApplicationsFound": "Aucune application trouvée", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Exclure les communiqués de SCENE des résultats", + "IndexerAlphaRatioSettingsExcludeScene": "Exclure SCENE", + "IndexerBeyondHDSettingsLimitedOnly": "Limité seulement", + "IndexerBeyondHDSettingsRssKeyHelpText": "Clé RSS du site (trouvée dans My Security => Clé RSS)", + "IndexerFileListSettingsUsernameHelpText": "Nom d'utilisateur du site", + "IndexerFileListSettingsPasskeyHelpText": "Site Passkey (Il s'agit de la chaîne alphanumérique dans l'url du tracker affichée dans votre client de téléchargement)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Doit avoir les permissions Utilisateur et Torrents", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Rechercher des publications par nom de groupe", + "IndexerHDBitsSettingsMediums": "Supports", + "IndexerHDBitsSettingsMediumsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", + "IndexerHDBitsSettingsUsernameHelpText": "Nom d'utilisateur du site", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent associé au cookie utilisé par le navigateur", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Paramètres supplémentaires de Newznab", + "IndexerNebulanceSettingsApiKeyHelpText": "Clé API à partir de Paramètres de l'utilisateur > Clés API. La clé doit avoir les permissions Liste et Téléchargement", + "IndexerNewznabSettingsApiKeyHelpText": "Clé API du site", + "IndexerNewznabSettingsVipExpirationHelpText": "Entrez la date (yyyy-mm-dd) pour l'expiration du VIP ou vide, {appName} notifiera 1 semaine après l'expiration du VIP", + "IndexerRedactedSettingsApiKeyHelpText": "Clé API du site (dans Paramètres => Paramètres d'accès)", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerSettingsAdditionalParameters": "Paramètres supplémentaires", + "IndexerSettingsApiPathHelpText": "Chemin d'accès à l'api, généralement {url}", + "IndexerSettingsApiUser": "Utilisateur de l'API", + "IndexerSettingsAppsMinimumSeeders": "Apps avec le nombre minimum de seeders disponibles", + "IndexerSettingsFreeleechOnly": "Freeleech seulement", + "IndexerSettingsGrabLimit": "Limite de saisie", + "IndexerSettingsQueryLimit": "Limite de requête", + "IndexerSettingsQueryLimitHelpText": "Le nombre de requêtes maximales tel que spécifié par l'unité respective que {appName} autorisera au site", + "IndexerSettingsRssKey": "Clé RSS", + "IndexerSettingsSeedRatioHelpText": "Le ratio qu'un torrent doit atteindre avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles des indexeurs", + "IndexerSettingsSeedTime": "Temps d'envoi", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Clé API du site", + "BlackholeFolderHelpText": "Dossier dans lequel {appName} stockera le fichier {extension}", + "DefaultCategory": "Catégorie par défaut", + "DownloadClientFloodSettingsAdditionalTags": "Étiquette supplémentaire", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Ajoute les propriétés des médias sous forme d'étiquette. Les conseils sont des exemples.", + "DownloadClientFreeboxSettingsApiUrl": "URL DE L'API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Définir l'URL de base de l'API Freebox avec la version de l'API, par exemple '{url}', par défaut '{defaultApiUrl}'", + "DownloadClientFloodSettingsTagsHelpText": "Étiquettes initiales d'un téléchargement. Pour être reconnu, un téléchargement doit avoir toutes les étiquettes initiales. Cela permet d'éviter les conflits avec des téléchargements non apparentés.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Ajoute d'un préfixe à l'API Flood, tel que {url}", + "DownloadClientFreeboxSettingsPortHelpText": "Port utilisé pour accéder à l'interface de la Freebox, la valeur par défaut est '{port}'", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Cette option nécessite au moins la version 16.0 de NzbGet", + "DownloadClientPneumaticSettingsNzbFolder": "Dossier Nzb", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Le jeton de l'application récupéré lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Nom d'hôte ou adresse IP de la Freebox, par défaut '{url}' (ne fonctionnera que si elle est sur le même réseau)", + "DownloadClientSettingsInitialState": "État initial", + "DownloadClientSettingsPriorityItemHelpText": "Priorité à utiliser lors de la saisie des articles", + "DownloadClientSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url {clientName}, tel que {url}", + "DownloadClientSettingsUseSslHelpText": "Utiliser une connexion sécurisée lors de la connexion à {clientName}", + "DownloadClientSettingsInitialStateHelpText": "État initial pour les torrents ajoutés à {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement de transmission par défaut", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url rpc de {clientName}, par exemple {url}, la valeur par défaut étant '{defaultUrl}'", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerBeyondHDSettingsApiKeyHelpText": "Clé API du site (dans My Security => Clé API)", + "IndexerBeyondHDSettingsRefundOnly": "Remboursement uniquement", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Recherche de remboursement seulement", + "IndexerBeyondHDSettingsRewindOnly": "Rembobiner seulement", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Recherche en arrière uniquement", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Recherche de freeleech uniquement (UL limitée)", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Sélectionnez les types de rejets qui vous intéressent. Si aucune option n'est sélectionnée, toutes les options sont utilisées.", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Clé API du site (dans Paramètres => Paramètres d'accès)", + "IndexerBeyondHDSettingsSearchTypes": "Types de recherche", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Afficher uniquement les versions freeleech", + "IndexerHDBitsSettingsOrigins": "Origines", + "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", + "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é).", + "IndexerSettingsGrabLimitHelpText": "Le nombre de prises maximales tel que spécifié par l'unité respective que {appName} autorisera sur le site", + "IndexerSettingsLimitsUnit": "Limites d'unité", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "Cookie du site", + "IndexerSettingsSeedTimeHelpText": "Durée pendant laquelle un torrent doit être envoyé avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement", + "IndexerSettingsSeedRatio": "Ratio d'envoi", + "IndexerSettingsVipExpiration": "Expiration de la carte VIP", + "SecretToken": "Jeton secret", + "TorrentBlackholeSaveMagnetFiles": "Enregistrer les fichiers magnétiques", + "TorrentBlackholeSaveMagnetFilesExtension": "Sauvegarde des fichiers magnétiques Extension", + "UsenetBlackholeNzbFolder": "Dossier Nzb", + "XmlRpcPath": "Chemin d'accès XML RPC", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Durée pendant laquelle un torrent de pack (saison ou discographie) doit être diffusé avant de s'arrêter, vide est la valeur par défaut de l'application", + "TorrentBlackholeSaveMagnetFilesHelpText": "Enregistrer le lien magnétique si aucun fichier .torrent n'est disponible (utile uniquement si le client de téléchargement prend en charge les liens magnétiques enregistrés dans un fichier)", + "IndexerId": "ID de l'indexeur", + "DownloadClientFreeboxSettingsAppId": "ID de l'application", + "DownloadClientFreeboxSettingsAppToken": "Jeton d'application", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Ce dossier devra être accessible depuis XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Dossier Strm", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Les fichiers .strm contenus dans ce dossier seront importés par drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Premier et dernier premiers", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "État initial des torrents ajoutés à qBittorrent. Notez que les torrents forcés ne respectent pas les restrictions relatives aux seeds", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordre séquentiel", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Téléchargement dans l'ordre séquentiel (qBittorrent 4.1.0+)", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "L'activation ajoutera des torrents et des magnets à rTorrent dans un état d'arrêt. Cela peut endommager les fichiers magnétiques.", + "DownloadClientRTorrentSettingsUrlPath": "Chemin d'url", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Chemin d'accès au point de terminaison XMLRPC, voir {url}. Il s'agit généralement de RPC2 ou de [chemin vers ruTorrent]{url2} lors de l'utilisation de ruTorrent.", + "DownloadClientRTorrentSettingsAddStopped": "Ajout arrêté", + "DownloadClientSettingsAddPaused": "Ajout en pause", + "DownloadClientSettingsDefaultCategoryHelpText": "Catégorie de secours par défaut si aucune catégorie mappée n'existe pour une version. L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée.", + "DownloadClientSettingsDestinationHelpText": "Spécifie manuellement la destination du téléchargement, laisser vide pour utiliser la destination par défaut", + "IndexerGazelleGamesSettingsSearchGroupNames": "Recherche de noms de groupes", + "IndexerHDBitsSettingsOriginsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", + "IndexerHDBitsSettingsUseFilenames": "Utiliser les noms de fichiers", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerSettingsApiPath": "Chemin d'accès à l'API", + "IndexerSettingsAppsMinimumSeedersHelpText": "Nombre minimum de seeders requis par les applications pour que l'indexeur s'en saisisse, vide est la valeur par défaut du profil Sync", + "IndexerSettingsBaseUrl": "Url de base", + "IndexerSettingsBaseUrlHelpText": "Sélectionnez l'url de base que {appName} utilisera pour les requêtes vers le site", + "IndexerSettingsLimitsUnitHelpText": "L'unité de temps pour le comptage des limites par indexeur", + "IndexerSettingsPackSeedTime": "Temps de seed du pack", + "IndexerSettingsPasskey": "Clé de passage", + "LabelIsRequired": "L'étiquette est requise", + "DownloadClientFreeboxSettingsAppIdHelpText": "L'ID de l'application donné lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_id')", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Télécharger d'abord le premier et le dernier morceau (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Utilisez une connexion sécurisée. Voir Options -> UI Web -> 'Utiliser HTTPS au lieu de HTTP' dans qBittorrent.", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension à utiliser pour les liens magnétiques, la valeur par défaut est '.magnet'", + "TorrentBlackholeTorrentFolder": "Dossier Torrent", + "UseSsl": "Utiliser SSL", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Emplacement facultatif dans lequel placer les téléchargements. Laisser vide pour utiliser l'emplacement par défaut de rTorrent", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Catégorie de secours par défaut si aucune catégorie mappée n'existe pour une version. L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée. Crée un sous-répertoire [catégorie] dans le répertoire de sortie.", + "ProwlarrDownloadClientsInAppOnlyAlert": "Les clients de téléchargement servent uniquement à effectuer des recherches dans l'application {appName} et ne se synchronisent pas avec les applications. Il n'est pas prévu d'ajouter une telle fonctionnalité.", + "Donate": "Donation", + "Menu": "Menu", + "Mixed": "Mixte", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Synchroniser le rejet des hachages torrent sur liste bloquée lors de la saisie", + "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)", + "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", + "OverrideAndAddToDownloadClient": "Remplacer et ajouter à la file d'attente de téléchargement", + "OverrideGrabModalTitle": "Remplacer et récupérer - {title}", + "PrioritySettings": "Priorité : {priority}", + "SelectDownloadClientModalTitle": "{modalTitle} – Sélectionnez le client de téléchargement", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "ProxyValidationBadRequest": "Échec du test du proxy. Code d'état : {statusCode}", + "ProxyValidationUnableToConnect": "Impossible de se connecter à l'indexeur : {exceptionMessage}. Vérifiez le journal pour plus de détails sur cette erreur", + "Default": "Par défaut", + "GrabRelease": "Saisir Release", + "ManualGrab": "Saisie manuelle", + "Open": "Ouvrir", + "Any": "Tous", + "BuiltIn": "Intégré", + "Script": "Script", + "InfoUrl": "URL d'informations", + "Redirected": "Rediriger", + "PublishedDate": "Date de publication", + "AllSearchResultsHiddenByFilter": "Tous les résultats sont masqués par le filtre appliqué.", + "AverageGrabs": "Prises moyennes", + "AverageQueries": "Requêtes moyennes", + "PackageVersionInfo": "{packageVersion} par {packageAuthor}", + "HealthMessagesInfoBox": "Vous pouvez trouver plus d'informations sur la cause de ces messages de contrôle de santé en cliquant sur le lien wiki (icône de livre) à la fin de la ligne, ou en vérifiant vos [journaux]({link}). Si vous rencontrez des difficultés pour interpréter ces messages, vous pouvez contacter notre support, via les liens ci-dessous.", + "LogSizeLimit": "Limite de taille du journal", + "LogSizeLimitHelpText": "Taille maximale du fichier journal en Mo avant archivage. La valeur par défaut est de 1 Mo.", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Rechercher les publications freeleech uniquement", + "IndexerAvistazSettingsUsernameHelpText": "Nom d'utilisateur du site", + "DockerUpdater": "Mettez à jour le conteneur Docker pour recevoir la mise à jour", + "Download": "Téléchargement", + "ErrorRestoringBackup": "Erreur lors de la restauration de la sauvegarde", + "ExternalUpdater": "{appName} est configuré pour utiliser un mécanisme de mise à jour externe", + "FailedToFetchUpdates": "Échec de la récupération des mises à jour", + "LogFilesLocation": "Les fichiers journaux sont situés dans : {location}", + "Logout": "Se déconnecter", + "NoEventsFound": "Aucun événement trouvé", + "RestartReloadNote": "Remarque : {appName} redémarrera et rechargera automatiquement l'interface utilisateur pendant le processus de restauration.", + "TheLogLevelDefault": "Le niveau de journalisation est par défaut à « Information » et peut être modifié dans les [paramètres généraux](/settings/general)", + "UpdateAppDirectlyLoadError": "Impossible de mettre à jour directement {appName},", + "UpdaterLogFiles": "Journaux du programme de mise à jour", + "WouldYouLikeToRestoreBackup": "Souhaitez-vous restaurer la sauvegarde « {name} » ?", + "AptUpdater": "Utiliser apt pour installer la mise à jour", + "Install": "Installer", + "InstallLatest": "Installer la dernière", + "InstallMajorVersionUpdateMessageLink": "Veuillez consulter [{domain}]({url}) pour plus d'informations.", + "InstallMajorVersionUpdate": "Installer la mise à jour", + "InstallMajorVersionUpdateMessage": "Cette mise à jour installera une nouvelle version majeure et pourrait ne pas être compatible avec votre système. Êtes-vous sûr de vouloir installer cette mise à jour ?", + "FailedToFetchSettings": "Échec de la récupération des paramètres", + "IndexerSettingsPreferMagnetUrlHelpText": "Si activé, cet indexeur privilégiera si possible l'usage de liens de type magnet aux liens torrent", + "PreferMagnetUrl": "Privilégier les liens de type magnet", + "IndexerAvistazSettingsPidHelpText": "PID de la page Mon compte ou Mon profil", + "IndexerAvistazSettingsPasswordHelpText": "Mot de passe du site", + "PreferMagnetUrlHelpText": "Si activé, cet indexeur privilégiera si possible l'usage de liens de type magnet aux liens torrent", + "PreviouslyInstalled": "Installé précédemment", + "CurrentlyInstalled": "Actuellement installé", + "IndexerSettingsPreferMagnetUrl": "URL de préférence Magnet", + "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 un historique d'inclusion de cryptomineurs, de logiciels malveillants et de publicités. Nous vous recommandons fortement de choisir un autre client." } diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 96cd0c06d..e46e5c09c 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -20,7 +20,7 @@ "NoLinks": "אין קישורים", "PendingChangesDiscardChanges": "מחק שינויים ועזוב", "ProxyBypassFilterHelpText": "השתמש ב- ',' כמפריד וב- '*.' כתו כללי לתת-דומיינים", - "ProxyCheckBadRequestMessage": "נכשל בדיקת ה- proxy. קוד קוד: {0}", + "ProxyBadRequestHealthCheckMessage": "נכשל בדיקת ה- proxy. קוד קוד: {statusCode}", "ReleaseStatus": "שחרור סטטוס", "Reload": "לִטעוֹן מִחָדָשׁ", "RemovedFromTaskQueue": "הוסר מתור המשימות", @@ -31,7 +31,7 @@ "SettingsEnableColorImpairedModeHelpText": "סגנון שונה כדי לאפשר למשתמשים לקויי צבע להבחין טוב יותר במידע המקודד בצבע", "SettingsLongDateFormat": "פורמט תאריך ארוך", "SettingsShortDateFormat": "פורמט תאריך קצר", - "UnableToLoadDownloadClients": "לא ניתן לטעון לקוחות הורדות", + "DownloadClientsLoadError": "לא ניתן לטעון לקוחות הורדות", "UnableToAddANewNotificationPleaseTryAgain": "לא ניתן להוסיף התראה חדשה, נסה שוב.", "UnableToLoadGeneralSettings": "לא ניתן לטעון את ההגדרות הכלליות", "About": "אודות", @@ -39,8 +39,8 @@ "Added": "נוסף", "Component": "רְכִיב", "Info": "מידע", - "IndexerLongTermStatusCheckAllClientMessage": "כל האינדקסים אינם זמינים עקב כשלים במשך יותר מ -6 שעות", - "IndexerLongTermStatusCheckSingleClientMessage": "אינדקסים לא זמינים עקב כשלים במשך יותר משש שעות: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "כל האינדקסים אינם זמינים עקב כשלים במשך יותר מ -6 שעות", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "אינדקסים לא זמינים עקב כשלים במשך יותר משש שעות: {indexerNames}", "Manual": "מדריך ל", "PageSize": "גודל עמוד", "PageSizeHelpText": "מספר הפריטים להצגה בכל עמוד", @@ -56,7 +56,7 @@ "Presets": "הגדרות קבועות מראש", "Proxy": "פרוקסי", "ProxyType": "סוג proxy", - "RSSIsNotSupportedWithThisIndexer": "RSS אינו נתמך עם אינדקס זה", + "RssIsNotSupportedWithThisIndexer": "RSS אינו נתמך עם אינדקס זה", "SettingsShowRelativeDatesHelpText": "הצג תאריכים קרובים (היום / אתמול / וכו ') או תאריכים מוחלטים", "TableOptions": "אפשרויות טבלה", "Tasks": "משימות", @@ -68,7 +68,7 @@ "UILanguageHelpTextWarning": "חובה לטעון דפדפן", "UISettings": "הגדרות ממשק המשתמש", "UnableToAddANewAppProfilePleaseTryAgain": "לא ניתן להוסיף פרופיל איכות חדש, נסה שוב.", - "UnableToLoadBackups": "לא ניתן לטעון גיבויים", + "BackupsLoadError": "לא ניתן לטעון גיבויים", "UnableToLoadTags": "לא ניתן לטעון תגים", "UnableToLoadUISettings": "לא ניתן לטעון הגדרות ממשק משתמש", "UnsavedChanges": "שינויים שלא נשמרו", @@ -80,8 +80,8 @@ "Custom": "המותאם אישית", "CustomFilters": "מסננים מותאמים אישית", "Date": "תַאֲרִיך", - "DownloadClientStatusCheckAllClientMessage": "כל לקוחות ההורדה אינם זמינים עקב כשלים", - "DownloadClientStatusCheckSingleClientMessage": "הורדת לקוחות לא זמינה עקב כשלים: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "כל לקוחות ההורדה אינם זמינים עקב כשלים", + "DownloadClientStatusSingleClientHealthCheckMessage": "הורדת לקוחות לא זמינה עקב כשלים: {downloadClientNames}", "Fixed": "תוקן", "FocusSearchBox": "תיבת חיפוש פוקוס", "Folder": "תיקיה", @@ -111,7 +111,7 @@ "Usenet": "Usenet", "URLBase": "בסיס URL", "Retention": "הַחזָקָה", - "RSS": "RSS", + "Rss": "RSS", "Save": "להציל", "SaveChanges": "שמור שינויים", "SaveSettings": "שמור הגדרות", @@ -122,7 +122,7 @@ "SettingsShowRelativeDates": "הצג תאריכים יחסית", "SuggestTranslationChange": "הצע שינוי בתרגום", "System": "מערכת", - "SystemTimeCheckMessage": "זמן המערכת אינו פעיל יותר מיום אחד. משימות מתוזמנות עשויות שלא לפעול כראוי עד לתיקון הזמן", + "SystemTimeHealthCheckMessage": "זמן המערכת אינו פעיל יותר מיום אחד. משימות מתוזמנות עשויות שלא לפעול כראוי עד לתיקון הזמן", "TableOptionsColumnsMessage": "בחר אילו עמודות גלויות ובאיזה סדר הן יופיעו", "TagCannotBeDeletedWhileInUse": "לא ניתן למחוק בזמן השימוש", "TagIsNotUsedAndCanBeDeleted": "לא משתמשים בתג וניתן למחוק אותו", @@ -141,7 +141,7 @@ "Time": "זְמַן", "Title": "כותרת", "UnableToLoadHistory": "לא ניתן לטעון את ההיסטוריה", - "UpdateCheckStartupNotWritableMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ההפעלה '{0}' אינה ניתנת לכתיבה על ידי המשתמש '{1}'.", + "UpdateStartupNotWritableHealthCheckMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ההפעלה '{startupFolder}' אינה ניתנת לכתיבה על ידי המשתמש '{userName}'.", "AddDownloadClient": "הוסף לקוח הורדות", "Filename": "שם קובץ", "Files": "קבצים", @@ -152,7 +152,7 @@ "Grabbed": "תפס", "Grabs": "לִתְפּוֹס", "Health": "בְּרִיאוּת", - "HealthNoIssues": "אין בעיות בתצורה שלך", + "NoIssuesWithYourConfiguration": "אין בעיות בתצורה שלך", "HideAdvanced": "הסתר מתקדם", "History": "הִיסטוֹרִיָה", "HomePage": "דף הבית", @@ -160,15 +160,15 @@ "Hostname": "שם מארח", "IndexerPriority": "עדיפות אינדקס", "IndexerPriorityHelpText": "עדיפות אינדקס מ -1 (הגבוה ביותר) ל -50 (הנמוך ביותר). ברירת מחדל: 25.", - "IndexerProxyStatusCheckAllClientMessage": "כל הרשימות אינן זמינות בגלל כשלים", - "IndexerProxyStatusCheckSingleClientMessage": "אינדקסים לא זמינים בגלל כשלים: {0}", - "IndexerStatusCheckAllClientMessage": "כל האינדקסים אינם זמינים עקב כשלים", - "IndexerStatusCheckSingleClientMessage": "אינדקסים לא זמינים בגלל כשלים: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "כל הרשימות אינן זמינות בגלל כשלים", + "IndexerProxyStatusUnavailableHealthCheckMessage": "אינדקסים לא זמינים בגלל כשלים: {indexerProxyNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "כל האינדקסים אינם זמינים עקב כשלים", + "IndexerStatusUnavailableHealthCheckMessage": "אינדקסים לא זמינים בגלל כשלים: {indexerNames}", "NoBackupsAreAvailable": "אין גיבויים", "PackageVersion": "גרסת חבילה", "Peers": "עמיתים", - "ProxyCheckFailedToTestMessage": "נכשל בדיקת ה- proxy: {0}", - "ProxyCheckResolveIpMessage": "פתרון כתובת ה- IP עבור מארח ה- Proxy המוגדר {0} נכשל", + "ProxyFailedToTestHealthCheckMessage": "נכשל בדיקת ה- proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "פתרון כתובת ה- IP עבור מארח ה- Proxy המוגדר {proxyHostName} נכשל", "ProxyPasswordHelpText": "עליך להזין שם משתמש וסיסמה רק אם נדרשים שם. השאר אותם ריקים אחרת.", "ProxyUsernameHelpText": "עליך להזין שם משתמש וסיסמה רק אם נדרשים שם. השאר אותם ריקים אחרת.", "Reddit": "רדיט", @@ -197,8 +197,8 @@ "Tomorrow": "מָחָר", "Torrent": "טורנטים", "UnableToAddANewIndexerProxyPleaseTryAgain": "לא ניתן להוסיף אינדקס חדש, נסה שוב.", - "UpdateCheckStartupTranslocationMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ההפעלה '{0}' נמצאת בתיקיית טרנסלוקציה של אפליקציות.", - "UpdateCheckUINotWritableMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ממשק המשתמש '{0}' אינה ניתנת לכתיבה על ידי המשתמש '{1}'.", + "UpdateStartupTranslocationHealthCheckMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ההפעלה '{startupFolder}' נמצאת בתיקיית טרנסלוקציה של אפליקציות.", + "UpdateUiNotWritableHealthCheckMessage": "לא ניתן להתקין את העדכון מכיוון שתיקיית ממשק המשתמש '{uiFolder}' אינה ניתנת לכתיבה על ידי המשתמש '{userName}'.", "Updates": "עדכונים", "UpdateScriptPathHelpText": "נתיב לסקריפט מותאם אישית שלוקח חבילת עדכון שחולצה ומטפל בשארית תהליך העדכון", "Uptime": "זמן עבודה", @@ -272,7 +272,7 @@ "CloneProfile": "פרופיל שיבוט", "Close": "סגור", "CloseCurrentModal": "סגור את המודול הנוכחי", - "DBMigration": "הגירת DB", + "DatabaseMigration": "הגירת DB", "Delete": "לִמְחוֹק", "DeleteNotification": "מחק הודעה", "DeleteNotificationMessageText": "האם אתה בטוח שברצונך למחוק את ההודעה '{0}'?", @@ -371,7 +371,7 @@ "EditSyncProfile": "הוספת פרופיל סינכרון", "Notifications": "התראות", "Notification": "התראות", - "TheLatestVersionIsAlreadyInstalled": "הגרסה האחרונה של {appName} כבר מותקנת", + "OnLatestVersion": "הגרסה האחרונה של {appName} כבר מותקנת", "Remove": "לְהַסִיר", "Replace": "החלף", "AddApplication": "הוספת אפליקציה", @@ -386,7 +386,7 @@ "DeleteSelectedDownloadClients": "מחק את לקוח ההורדות", "DeleteSelectedIndexersMessageText": "האם אתה בטוח שברצונך למחוק את האינדקס '{0}'?", "DownloadClientPriorityHelpText": "העדיפו עדיפות למספר לקוחות הורדה. Round-Robin משמש ללקוחות עם אותה עדיפות.", - "ApiKeyValidationHealthCheckMessage": "עדכן בבקשה את מפתח ה־API שלך כדי שיהיה באורך של לפחות {0} תווים. תוכל לעשות זאת בהגדרות או דרך קובץ הקונפיגורציה.", + "ApiKeyValidationHealthCheckMessage": "עדכן בבקשה את מפתח ה־API שלך כדי שיהיה באורך של לפחות {length} תווים. תוכל לעשות זאת בהגדרות או דרך קובץ הקונפיגורציה.", "More": "יותר", "Track": "זֵכֶר", "ApplyTagsHelpTextHowToApplyApplications": "כיצד להחיל תגים על הסרטים שנבחרו", @@ -401,11 +401,36 @@ "Album": "אלבום", "Artist": "אמן", "NotificationStatusAllClientHealthCheckMessage": "כל הרשימות אינן זמינות בגלל כשלים", - "NotificationStatusSingleClientHealthCheckMessage": "רשימות לא זמינות בגלל כשלים: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "רשימות לא זמינות בגלל כשלים: {notificationNames}", "ResetAPIKeyMessageText": "האם אתה בטוח שברצונך לאפס את מפתח ה- API שלך?", "DisabledForLocalAddresses": "מושבת לכתובות מקומיות", "Publisher": "מוציא לאור", "None": "אף אחד", "AuthForm": "טפסים (דף כניסה)", - "AuthBasic": "בסיסי (חלון קופץ לדפדפן)" + "AuthBasic": "בסיסי (חלון קופץ לדפדפן)", + "IndexerHDBitsSettingsMediums": "בינוני", + "ProxyValidationBadRequest": "נכשל בדיקת ה- proxy. קוד קוד: {statusCode}", + "CustomFilter": "מסננים מותאמים אישית", + "GrabRelease": "שחרור תפוס", + "BuiltIn": "נִבנָה בְּ", + "Script": "תַסרִיט", + "PublishedDate": "תאריך פרסום", + "AddCategory": "הוסף קטגוריה", + "ActiveApps": "אפליקציות פעילות", + "ActiveIndexers": "אינדקסרים פעילים", + "AllSearchResultsHiddenByFilter": "כל התוצאות מוסתרות על ידי המסנן שהוחל", + "InstallLatest": "התקן את האחרונה", + "NoEventsFound": "לא נמצאו אירועים", + "DockerUpdater": "עדכן את מיכל העגינה לקבל את העדכון", + "Download": "הורד", + "ErrorRestoringBackup": "שגיאה בשחזור הגיבוי", + "ExternalUpdater": "{appName} מוגדר להשתמש במנגנון עדכון חיצוני", + "RestartReloadNote": "הערה: {appName} יופעל מחדש אוטומטית וטען מחדש את ממשק המשתמש במהלך תהליך השחזור.", + "UpdateAppDirectlyLoadError": "לא ניתן לעדכן את {appName} ישירות,", + "AptUpdater": "השתמש ב- apt כדי להתקין את העדכון", + "Clone": "סגור", + "CurrentlyInstalled": "מותקן כעת", + "Stats": "סטָטוּס", + "Season": "סיבה", + "Mixed": "תוקן" } diff --git a/src/NzbDrone.Core/Localization/Core/hi.json b/src/NzbDrone.Core/Localization/Core/hi.json index d667bc4c3..c37eb20e0 100644 --- a/src/NzbDrone.Core/Localization/Core/hi.json +++ b/src/NzbDrone.Core/Localization/Core/hi.json @@ -37,7 +37,7 @@ "Test": "परीक्षा", "Port": "बंदरगाह", "PortNumber": "पोर्ट संख्या", - "RSSIsNotSupportedWithThisIndexer": "RSS इस अनुक्रमणिका के साथ समर्थित नहीं है", + "RssIsNotSupportedWithThisIndexer": "RSS इस अनुक्रमणिका के साथ समर्थित नहीं है", "SendAnonymousUsageData": "अनाम उपयोग डेटा भेजें", "SetTags": "टैग सेट करें", "About": "के बारे में", @@ -54,7 +54,7 @@ "Authentication": "प्रमाणीकरण", "ClientPriority": "ग्राहक प्राथमिकता", "Connections": "सम्बन्ध", - "DownloadClientStatusCheckSingleClientMessage": "विफलताओं के कारण अनुपलब्ध ग्राहक डाउनलोड करें: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "विफलताओं के कारण अनुपलब्ध ग्राहक डाउनलोड करें: {downloadClientNames}", "NoChanges": "कोई बदलाव नहीं", "Yesterday": "बिता कल", "DeleteIndexerProxyMessageText": "क्या आप वाकई '{0}' टैग हटाना चाहते हैं?", @@ -104,14 +104,14 @@ "DownloadClientSettings": "क्लाइंट सेटिंग्स डाउनलोड करें", "EventType": "घटना प्रकार", "Exception": "अपवाद", - "IndexerStatusCheckAllClientMessage": "विफलताओं के कारण सभी अनुक्रमणिका अनुपलब्ध हैं", - "IndexerStatusCheckSingleClientMessage": "अनुक्रमणिका विफलताओं के कारण अनुपलब्ध: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "विफलताओं के कारण सभी अनुक्रमणिका अनुपलब्ध हैं", + "IndexerStatusUnavailableHealthCheckMessage": "अनुक्रमणिका विफलताओं के कारण अनुपलब्ध: {indexerNames}", "Info": "जानकारी", "Language": "भाषा: हिन्दी", "UnableToAddANewDownloadClientPleaseTryAgain": "नया डाउनलोड क्लाइंट जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।", "UnableToAddANewIndexerPleaseTryAgain": "नया अनुक्रमणिका जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।", "UnableToAddANewIndexerProxyPleaseTryAgain": "नया अनुक्रमणिका जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।", - "UnableToLoadBackups": "बैकअप लोड करने में असमर्थ", + "BackupsLoadError": "बैकअप लोड करने में असमर्थ", "NoTagsHaveBeenAddedYet": "अभी तक कोई टैग नहीं जोड़े गए हैं", "Reddit": "reddit", "UpdateMechanismHelpText": "रेडर के बिल्ट इन अपडेटर या स्क्रिप्ट का उपयोग करें", @@ -120,7 +120,7 @@ "New": "नया", "CustomFilters": "कस्टम फ़िल्टर", "Dates": "खजूर", - "DBMigration": "DB प्रवासन", + "DatabaseMigration": "DB प्रवासन", "DeleteApplicationMessageText": "क्या आप वाकई '{0}' की सूचना हटाना चाहते हैं?", "DeleteBackup": "बैकअप हटाएं", "DeleteBackupMessageText": "क्या आप वाकई '{0}' बैकअप हटाना चाहते हैं?", @@ -133,9 +133,9 @@ "NoLinks": "कोई लिंक नहीं", "BindAddress": "बाँध का पता", "Branch": "डाली", - "ProxyCheckBadRequestMessage": "प्रॉक्सी का परीक्षण करने में विफल। स्थिति कोड: {0}", - "ProxyCheckFailedToTestMessage": "प्रॉक्सी का परीक्षण करने में विफल: {0}", - "ProxyCheckResolveIpMessage": "कॉन्फ़िगर प्रॉक्सी होस्ट {0} के लिए आईपी एड्रेस को हल करने में विफल", + "ProxyBadRequestHealthCheckMessage": "प्रॉक्सी का परीक्षण करने में विफल। स्थिति कोड: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "प्रॉक्सी का परीक्षण करने में विफल: {url}", + "ProxyResolveIpHealthCheckMessage": "कॉन्फ़िगर प्रॉक्सी होस्ट {proxyHostName} के लिए आईपी एड्रेस को हल करने में विफल", "ProxyPasswordHelpText": "यदि आवश्यक हो तो आपको केवल एक उपयोगकर्ता नाम और पासवर्ड दर्ज करना होगा। उन्हें खाली छोड़ दें अन्यथा।", "ProxyType": "प्रॉक्सी प्रकार", "ProxyUsernameHelpText": "यदि आवश्यक हो तो आपको केवल एक उपयोगकर्ता नाम और पासवर्ड दर्ज करना होगा। उन्हें खाली छोड़ दें अन्यथा।", @@ -154,7 +154,7 @@ "Source": "स्रोत", "StartupDirectory": "स्टार्टअप निर्देशिका", "SuggestTranslationChange": "अनुवाद परिवर्तन का सुझाव दें", - "SystemTimeCheckMessage": "सिस्टम का समय 1 दिन से अधिक बंद है। जब तक समय सही नहीं होगा तब तक शेड्यूल किए गए कार्य सही तरीके से नहीं चल सकते हैं", + "SystemTimeHealthCheckMessage": "सिस्टम का समय 1 दिन से अधिक बंद है। जब तक समय सही नहीं होगा तब तक शेड्यूल किए गए कार्य सही तरीके से नहीं चल सकते हैं", "TableOptionsColumnsMessage": "चुनें कि कौन से कॉलम दिखाई दे रहे हैं और वे किस क्रम में दिखाई देते हैं", "TagsSettingsSummary": "सभी टैग देखें और उनका उपयोग कैसे किया जाता है। अप्रयुक्त टैग को हटाया जा सकता है", "TestAll": "सभी का परीक्षण करें", @@ -170,7 +170,7 @@ "UnsavedChanges": "बिना बदलाव किए", "UnselectAll": "सभी का चयन रद्द", "UpdateAutomaticallyHelpText": "अपडेट को स्वचालित रूप से डाउनलोड और इंस्टॉल करें। आप अभी भी सिस्टम से अपडेट कर पाएंगे: अपडेट", - "UpdateCheckUINotWritableMessage": "अद्यतन स्थापित नहीं कर सकता क्योंकि UI फ़ोल्डर '{0}' उपयोगकर्ता '{1}' द्वारा लिखने योग्य नहीं है।", + "UpdateUiNotWritableHealthCheckMessage": "अद्यतन स्थापित नहीं कर सकता क्योंकि UI फ़ोल्डर '{uiFolder}' उपयोगकर्ता '{userName}' द्वारा लिखने योग्य नहीं है।", "Uptime": "अपटाइम", "URLBase": "URL बेस", "YesCancel": "हाँ, रद्द करें", @@ -192,7 +192,7 @@ "RestoreBackup": "बैकअप बहाल", "Result": "परिणाम", "Retention": "अवधारण", - "RSS": "आरएसएस", + "Rss": "आरएसएस", "ScriptPath": "पटकथा पथ", "Search": "खोज", "SettingsTimeFormat": "समय प्रारूप", @@ -234,7 +234,7 @@ "CloneProfile": "क्लोन प्रोफ़ाइल", "Close": "बंद करे", "CloseCurrentModal": "वर्तमान मोडल को बंद करें", - "DownloadClientStatusCheckAllClientMessage": "सभी डाउनलोड क्लाइंट विफलताओं के कारण अनुपलब्ध हैं", + "DownloadClientStatusAllClientHealthCheckMessage": "सभी डाउनलोड क्लाइंट विफलताओं के कारण अनुपलब्ध हैं", "Edit": "संपादित करें", "EnableAutomaticSearch": "स्वचालित खोज सक्षम करें", "EnableAutomaticSearchHelpText": "यूआई के माध्यम से या रेडर द्वारा स्वचालित खोज किए जाने पर उपयोग किया जाएगा", @@ -260,7 +260,7 @@ "Grabbed": "पकड़ा", "Grabs": "लपकना", "Health": "स्वास्थ्य", - "HealthNoIssues": "आपके कॉन्फ़िगरेशन के साथ कोई समस्या नहीं है", + "NoIssuesWithYourConfiguration": "आपके कॉन्फ़िगरेशन के साथ कोई समस्या नहीं है", "HideAdvanced": "उन्नत छिपाएँ", "History": "इतिहास", "HomePage": "मुख पृष्ठ", @@ -268,12 +268,12 @@ "Hostname": "होस्ट का नाम", "IncludeHealthWarningsHelpText": "स्वास्थ्य चेतावनी शामिल करें", "IndexerFlags": "इंडेक्स फ्लैग", - "IndexerLongTermStatusCheckAllClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सभी सूचकांक अनुपलब्ध हैं", - "IndexerLongTermStatusCheckSingleClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सूचकांक उपलब्ध नहीं: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सभी सूचकांक अनुपलब्ध हैं", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सूचकांक उपलब्ध नहीं: {indexerNames}", "IndexerPriority": "सूचकांक प्राथमिकता", "IndexerPriorityHelpText": "इंडेक्सर प्राथमिकता 1 (उच्चतम) से 50 (सबसे कम)। डिफ़ॉल्ट: 25", - "IndexerProxyStatusCheckAllClientMessage": "सभी सूचियाँ विफल होने के कारण अनुपलब्ध हैं", - "IndexerProxyStatusCheckSingleClientMessage": "अनुक्रमणिका विफलताओं के कारण अनुपलब्ध: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "सभी सूचियाँ विफल होने के कारण अनुपलब्ध हैं", + "IndexerProxyStatusUnavailableHealthCheckMessage": "अनुक्रमणिका विफलताओं के कारण अनुपलब्ध: {indexerProxyNames}", "InteractiveSearch": "इंटरएक्टिव खोज", "Interval": "मध्यान्तर", "KeyboardShortcuts": "कुंजीपटल अल्प मार्ग", @@ -294,11 +294,11 @@ "RestartNow": "अब पुनःचालू करें", "SettingsEnableColorImpairedMode": "रंग-बिगड़ा मोड सक्षम करें", "UnableToAddANewNotificationPleaseTryAgain": "नई अधिसूचना जोड़ने में असमर्थ, कृपया पुनः प्रयास करें।", - "UnableToLoadDownloadClients": "डाउनलोड क्लाइंट लोड करने में असमर्थ", + "DownloadClientsLoadError": "डाउनलोड क्लाइंट लोड करने में असमर्थ", "UnableToLoadGeneralSettings": "सामान्य सेटिंग्स लोड करने में असमर्थ", "UnableToLoadNotifications": "सूचनाएं लोड करने में असमर्थ", - "UpdateCheckStartupNotWritableMessage": "अपडेट स्थापित नहीं किया जा सकता क्योंकि स्टार्टअप फ़ोल्डर '{0}' उपयोगकर्ता '{1}' द्वारा लिखने योग्य नहीं है।", - "UpdateCheckStartupTranslocationMessage": "अपडेट स्थापित नहीं किया जा सकता क्योंकि स्टार्टअप फ़ोल्डर '{0}' ऐप ट्रांसलेशन फ़ोल्डर में है।", + "UpdateStartupNotWritableHealthCheckMessage": "अपडेट स्थापित नहीं किया जा सकता क्योंकि स्टार्टअप फ़ोल्डर '{startupFolder}' उपयोगकर्ता '{userName}' द्वारा लिखने योग्य नहीं है।", + "UpdateStartupTranslocationHealthCheckMessage": "अपडेट स्थापित नहीं किया जा सकता क्योंकि स्टार्टअप फ़ोल्डर '{startupFolder}' ऐप ट्रांसलेशन फ़ोल्डर में है।", "NoUpdatesAreAvailable": "कोई अद्यतन उपलब्ध नहीं हैं", "OAuthPopupMessage": "आपके ब्राउज़र द्वारा पॉप-अप्स को ब्लॉक किया जा रहा है", "OnHealthIssueHelpText": "स्वास्थ्य के मुद्दे पर", @@ -328,7 +328,7 @@ "LastExecution": "अंतिम निष्पादन", "Queued": "कतारबद्ध", "Remove": "हटाना", - "TheLatestVersionIsAlreadyInstalled": "रेडर का नवीनतम संस्करण पहले से ही स्थापित है", + "OnLatestVersion": "रेडर का नवीनतम संस्करण पहले से ही स्थापित है", "Replace": "बदलने के", "More": "अधिक", "DeleteSelectedDownloadClients": "डाउनलोड क्लाइंट हटाएं", @@ -347,10 +347,31 @@ "minutes": "मिनट", "DeleteAppProfileMessageText": "क्या आप वाकई गुणवत्ता प्रोफ़ाइल {0} को हटाना चाहते हैं", "NotificationStatusAllClientHealthCheckMessage": "सभी सूचियाँ विफल होने के कारण अनुपलब्ध हैं", - "NotificationStatusSingleClientHealthCheckMessage": "विफलताओं के कारण अनुपलब्ध सूची: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "विफलताओं के कारण अनुपलब्ध सूची: {notificationNames}", "AuthBasic": "बेसिक (ब्राउज़र पॉपअप)", "AuthForm": "प्रपत्र (लॉग इन पेज)", "DisabledForLocalAddresses": "स्थानीय पते के लिए अक्षम", "None": "कोई नहीं", - "ResetAPIKeyMessageText": "क्या आप वाकई अपनी API कुंजी को रीसेट करना चाहते हैं?" + "ResetAPIKeyMessageText": "क्या आप वाकई अपनी API कुंजी को रीसेट करना चाहते हैं?", + "IndexerHDBitsSettingsMediums": "मध्यम", + "CustomFilter": "कस्टम फ़िल्टर", + "ProxyValidationBadRequest": "प्रॉक्सी का परीक्षण करने में विफल। स्थिति कोड: {statusCode}", + "GrabRelease": "पकड़ो रिलीज", + "BuiltIn": "में निर्मित", + "Script": "लिपि", + "PublishedDate": "प्रकाशित तिथि", + "AllSearchResultsHiddenByFilter": "सभी परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं", + "AptUpdater": "अद्यतन स्थापित करने के लिए उपयुक्त का उपयोग करें", + "DockerUpdater": "अपडेट प्राप्त करने के लिए docker कंटेनर को अपडेट करें", + "Download": "डाउनलोड", + "ErrorRestoringBackup": "बैकअप बहाल करने में त्रुटि", + "NoEventsFound": "कोई घटना नहीं मिली", + "RestartReloadNote": "नोट: रैडियर स्वचालित रूप से पुनः आरंभ करेगा और पुनर्स्थापना प्रक्रिया के दौरान UI को फिर से लोड करेगा।", + "UpdateAppDirectlyLoadError": "सीधे {appName} अद्यतन करने में असमर्थ,", + "InstallLatest": "नवीनतम स्थापित करें", + "Clone": "बंद करे", + "CurrentlyInstalled": "वर्तमान में स्थापित है", + "Mixed": "फिक्स्ड", + "Season": "कारण", + "Stats": "स्थिति" } diff --git a/src/NzbDrone.Core/Localization/Core/hr.json b/src/NzbDrone.Core/Localization/Core/hr.json index c4113b3d7..52e5603e9 100644 --- a/src/NzbDrone.Core/Localization/Core/hr.json +++ b/src/NzbDrone.Core/Localization/Core/hr.json @@ -27,7 +27,7 @@ "Remove": "Ukloni", "Replace": "Zamijeni", "Reset": "Resetiraj", - "RSS": "RSS", + "Rss": "RSS", "Scheduled": "Zakazano", "Search": "Traži", "Seeders": "Seederi", @@ -174,5 +174,43 @@ "DisabledForLocalAddresses": "Onemogućeno za Lokalne Adrese", "ResetAPIKeyMessageText": "Jeste li sigurni da želite resetirati vaš API Ključ?", "AuthBasic": "Osnovno (Skočni prozor preglednika)", - "AuthForm": "Forme (Login Stranica)" + "AuthForm": "Forme (Login Stranica)", + "Id": "ID", + "CountApplicationsSelected": "{count} Kolekcija odabrano", + "IndexerHDBitsSettingsCodecs": "Kodek", + "Directory": "Direktorij", + "BuiltIn": "Ugrađeno", + "Redirected": "Preusmjeri", + "AllSearchResultsHiddenByFilter": "Svi rezultati su skriveni zbog primjenjenog filtera", + "ApplyChanges": "Primjeni Promjene", + "ApiKeyValidationHealthCheckMessage": "Molimo ažuriraj svoj API ključ da ima barem {length} znakova. Ovo možeš uraditi u postavkama ili konfiguracijskoj datoteci", + "AppUpdated": "{appName} Ažuriran", + "AuthenticationRequired": "Potrebna Autentikacija", + "AddConnection": "Dodaj vezu", + "AddDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}", + "AddIndexerImplementation": "Dodaj Indexer - {implementationName}", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrdi novu lozinku", + "AuthenticationRequiredPasswordHelpTextWarning": "Unesi novu lozinku", + "AddConnectionImplementation": "Dodaj Vezu - {implementationName}", + "Any": "BIlo koji", + "AppUpdatedVersion": "{appName} je ažuriran na verziju '{version}', kako bi najnovije promjene bile aktivne potrebno je ponovno učitati {appName}", + "AuthenticationMethod": "Metoda Autentikacije", + "AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime", + "AuthenticationMethodHelpTextWarning": "Molimo odaberi ispravnu metodu autentikacije", + "AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa.", + "UnableToAddANewIndexerPleaseTryAgain": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.", + "UnableToAddANewNotificationPleaseTryAgain": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno.", + "UnableToAddANewDownloadClientPleaseTryAgain": "Nesupješno dodavanje klijenta za preuzimanje, molimo pokušaj ponovno.", + "EditDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}", + "EditConnectionImplementation": "Dodaj Vezu - {implementationName}", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.", + "AddApplicationImplementation": "Dodaj Vezu - {implementationName}", + "UnableToAddANewAppProfilePleaseTryAgain": "Neuspješno dodavanje novog profila kvalitete, molimo pokušaj ponovno.", + "EditIndexerImplementation": "Dodaj Indexer - {implementationName}", + "AddIndexerProxyImplementation": "Dodaj Indexer - {implementationName}", + "UnableToAddANewApplicationPleaseTryAgain": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno.", + "EditApplicationImplementation": "Dodaj Vezu - {implementationName}", + "AptUpdater": "Koristi apt kako bi instalirao ažuriranje", + "EditIndexerProxyImplementation": "Dodaj Indexer - {implementationName}", + "Clone": "Zatvori" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index f74489ced..ea8d13566 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1,103 +1,103 @@ { "About": "Névjegy", "Analytics": "Analitika", - "AddIndexer": "Indexer hozzáadása", + "AddIndexer": "Indexelő hozzáadása", "AddingTag": "Címke hozzáadása", "Error": "Hiba", - "DeleteTag": "Címke Törlése", - "EnableAutomaticSearchHelpText": "Akkor kerül felhasználásra, ha az automatikus kereséseket a kezelőfelületen vagy a {appName}-on keresztül hajtják végre", + "DeleteTag": "Címke törlése", + "EnableAutomaticSearchHelpText": "Akkor lesz használatos, ha automatikus keresést hajt végre a felhasználói felületen vagy a(z) {appName} alkalmazáson keresztül", "EnableAutomaticSearch": "Engedélyezd az Automatikus Keresést", "Enable": "Aktiválás", "EditIndexer": "Indexer Szerkesztése", - "Edit": "Szerkesztés", - "DownloadClientStatusCheckSingleClientMessage": "Letöltőkliens hiba miatt nem elérhető: {0}", - "DownloadClientStatusCheckAllClientMessage": "Az összes letöltőkliens elérhetetlen, hiba miatt", + "Edit": "Szerkeszt", + "DownloadClientStatusSingleClientHealthCheckMessage": "Letöltőkliens hiba miatt nem elérhető: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltőkliens elérhetetlen, hiba miatt", "DownloadClientsSettingsSummary": "Letöltőkliens konfigurációja a {appName} felhasználói felület keresésbe történő integráláshoz", "DownloadClientSettings": "Letöltőkliens Beállítások", - "DownloadClients": "Letöltőkliensek", - "DownloadClient": "Letöltési Kliens", + "DownloadClients": "Letöltő kliensek", + "DownloadClient": "Letöltési kliens", "Docker": "Docker", - "Disabled": "Letiltott", - "Details": "Részletek", - "DeleteTagMessageText": "Biztosan törlöd a(z) „{0}” címkét?", - "DeleteNotificationMessageText": "Biztosan törlöd a(z) „{0}” értesítést?", - "DeleteNotification": "Értesítés Törlése", - "DeleteDownloadClientMessageText": "Biztosan törlöd a(z) „{0}” letöltő klienst?", - "DeleteDownloadClient": "Letöltőkliens Törlése", - "DeleteBackupMessageText": "Biztosan törlöd a(z) „{0}” biztonsági mentést?", - "DeleteBackup": "Biztonsági Mentés Törlése", + "Disabled": "Tiltva", + "Details": "részletek", + "DeleteTagMessageText": "Biztosan törli a „{label}” címkét?", + "DeleteNotificationMessageText": "Biztosan törli a(z) „{name}” értesítést?", + "DeleteNotification": "Értesítés törlése", + "DeleteDownloadClientMessageText": "Biztosan törli a(z) \"{name}\" letöltési klienst?", + "DeleteDownloadClient": "Letöltőkliens törlése", + "DeleteBackupMessageText": "Biztosan törli a '{name}' biztonsági mentést?", + "DeleteBackup": "Biztonsági Mentés törlése", "Delete": "Törlés", - "DBMigration": "DB Migráció", + "DatabaseMigration": "Adatbázis-migráció", "Dates": "Dátumok", "Date": "Dátum", - "CustomFilters": "Egyéni Szűrők", + "CustomFilters": "Egyedi Szűrők", "ConnectSettingsSummary": "Értesítések és egyéni szkriptek", - "ConnectSettings": "Kapcsolódási Beállítások", + "ConnectSettings": "Csatlakozási beállítások", "Connections": "Kapcsolatok", - "ConnectionLost": "Kapcsolódás Elveszett", + "ConnectionLost": "A kapcsolat megszakadt", "Connect": "Értesítések", - "Component": "Komponens", + "Component": "Összetevő", "Columns": "Oszlopok", "CloseCurrentModal": "Aktuális Mód Bezárása", "Close": "Bezárás", - "CloneProfile": "Profil Klónozása", - "ClientPriority": "Kliens Prioritás", + "CloneProfile": "Profil klónozása", + "ClientPriority": "Kliens prioritás", "Clear": "Törölni", "ChangeHasNotBeenSavedYet": "A változások még nem lettek elmentve", "CertificateValidationHelpText": "Módosítsa a HTTPS tanúsítás szigorúságát", "CertificateValidation": "Tanúsítvány érvényesítése", - "CancelPendingTask": "Biztosan törlöd ezt a függőben lévő feladatot?", + "CancelPendingTask": "Biztosan törölni szeretné ezt a függőben lévő feladatot?", "Cancel": "Mégse", "BypassProxyForLocalAddresses": "Proxy megkerülése a helyi hálózatos címekhez", - "BranchUpdateMechanism": "A külső frissítési mechanizmus által használt ágazat", - "BranchUpdate": "Ágazattípus a {appName} frissítéseihez", + "BranchUpdateMechanism": "Külső frissítési mechanizmus által használt ág", + "BranchUpdate": "A(z) {appName} frissítéséhez használt fiók", "Branch": "Ágazat", "BindAddressHelpText": "Érvényes IP-cím, localhost vagy '*' minden interfészhez", - "BindAddress": "Kapcsolási Cím", - "BeforeUpdate": "Alkalmazásfrissítés előtt", + "BindAddress": "Kötési cím", + "BeforeUpdate": "Frissítés előtt", "Backups": "Biztonsági mentések", - "BackupRetentionHelpText": "A megőrzési időnél régebbi automatikus biztonsági másolatok automatikusan törlésre kerülnek", - "BackupNow": "Biztonsági Mentés Most", - "BackupIntervalHelpText": "Időeltérés a biztonsági mentések között", - "BackupFolderHelpText": "Az elérési útvonalak a {appName} AppData könyvtárában lesznek", - "Backup": "Biztonsági Mentés", + "BackupRetentionHelpText": "A megőrzési időszaknál régebbi automatikus biztonsági másolatok automatikusan törlődnek", + "BackupNow": "Biztonsági Mentés", + "BackupIntervalHelpText": "Az automatikus biztonsági mentések közötti időköz", + "BackupFolderHelpText": "A relatív elérési utak a(z) {appName} AppData könyvtárában találhatók", + "Backup": "Biztonsági mentés", "AutomaticSearch": "Automatikus keresés", "Automatic": "Automatikus", - "AnalyticsEnabledHelpText": "Küldjön névtelen használati és hibainformációkat a {appName} szervereire. Ez magában foglalja a böngészőjéről szóló információkat, mely {appName} WebUI oldalakat használja, a hibajelentést, valamint az operációs rendszer adatait. Ezeket az információkat a funkciók és a hibajavítások rangsorolására használjuk fel.", - "AuthenticationMethodHelpText": "Felhasználónév és Jelszó szükséges a {appName}-hoz való hozzáféréshez", + "AnalyticsEnabledHelpText": "Névtelen használati és hibainformáció küldése {appName} szervereinek. Ez magában foglalja a böngészővel kapcsolatos információkat, a használt {appName} WebUI oldalakat, a hibajelentéseket, valamint az operációs rendszert és a futásidejű verziót. Ezeket az információkat a funkciók és a hibajavítások fontossági sorrendjének meghatározására fogjuk használni.", + "AuthenticationMethodHelpText": "Felhasználónév és jelszó szükséges a(z) {appName} eléréséhez", "Authentication": "Hitelesítés", "ApplyTags": "Címkék alkalmazása", "Age": "Kor", "ApiKey": "API Kulcs", "All": "Összes", "AcceptConfirmationModal": "Változás Megerősítése", - "Apply": "Alkalmazás", - "AppDataLocationHealthCheckMessage": "A frissítés nem lehetséges anélkül hogy az AppData ne törlődjön", - "AppDataDirectory": "AppData Mappa", + "Apply": "Alkamaz", + "AppDataLocationHealthCheckMessage": "A frissítés nem lehetséges az alkalmazás adatok törlése nélkül", + "AppDataDirectory": "AppData Könyvtár", "Added": "Hozzáadva", "Actions": "Teendők", - "History": "Történet", + "History": "Előzmény", "HideAdvanced": "Haladó Elrejtése", - "HealthNoIssues": "Nincs hiba a konfigurációval", - "Health": "Állapot", + "NoIssuesWithYourConfiguration": "Nincs hiba a konfigurációval", + "Health": "Egészség", "GeneralSettingsSummary": "Port, SSL, felhasználónév / jelszó, proxy, elemzések, és frissítések", "ForMoreInformationOnTheIndividualDownloadClients": "Ha többet szeretnél megtudni a különböző letöltési kliensekről, kattints az információs gombokra.", "Folder": "Mappa", "FocusSearchBox": "Fókusz Keresőmező", - "Fixed": "Kijavítva", + "Fixed": "Rögzített", "FilterPlaceHolder": "Filmek Keresése", "Filter": "Szűrő", - "Files": "Fájl", - "Filename": "Fájlnév", - "Failed": "Sikertelen", - "ExistingTag": "Meglévő Címke", + "Files": "Fájlok", + "Filename": "Fájl név", + "Failed": "Nem sikerült", + "ExistingTag": "Létező címke", "Exception": "Kivétel", - "EventType": "Események Típusa", + "EventType": "Esemény típus", "Events": "Események", - "ErrorLoadingContents": "Hiba történt a tartalom betöltésekor", + "ErrorLoadingContents": "Hiba a tartalom betöltésekor", "EnableSslHelpText": " A hatálybalépéshez újra kell indítani rendszergazdaként", "EnableSSL": "SSL Engedélyezése", - "EnableInteractiveSearchHelpText": "Interaktív keresés esetén használható", + "EnableInteractiveSearchHelpText": "Interaktív keresés esetén lesz használatos", "EnableInteractiveSearch": "Interaktív Keresés Engedélyezése", "System": "Rendszer", "SuggestTranslationChange": "Javasolj fordítási változtatást", @@ -111,10 +111,10 @@ "SSLCertPasswordHelpText": "Jelszó a Pfx fájlhoz", "SSLCertPassword": "SSL Tanúsítvány jelszava", "Source": "Forrás", - "Sort": "Rendezés", + "Sort": "Fajta", "Size": "Méret", - "Shutdown": "Leállítás", - "ShowSearchHelpText": "A kereső gomb megjelenítése az egérrel", + "Shutdown": "Leállitás", + "ShowSearchHelpText": "Keresés gomb megjelenítése az egérrel", "ShowAdvanced": "Haladó nézet", "SettingsTimeFormat": "Időformátum", "SettingsShowRelativeDatesHelpText": "Relatív (Ma / Tegnap / stb.) vagy valós dátumok megjelenítése", @@ -125,7 +125,7 @@ "SettingsEnableColorImpairedMode": "Színtévesztő mód bekapcsolása", "Settings": "Beállítások", "SendAnonymousUsageData": "Névtelen használati adatok küldése", - "SelectAll": "Összes kijelölése", + "SelectAll": "Mindet kiválaszt", "Seeders": "Seederek", "Security": "Biztonság", "Search": "Keresés", @@ -134,97 +134,97 @@ "SaveSettings": "Beállítások mentése", "SaveChanges": "Változtatások mentése", "Save": "Mentés", - "RSSIsNotSupportedWithThisIndexer": "Az RSS nem támogatott ezzel az indexerrel", + "RssIsNotSupportedWithThisIndexer": "Ez az indexelő nem támogatja az RSS-t", "Retention": "Visszatartás", - "Result": "Eredmények", + "Result": "Eredmény", "RestoreBackup": "Biztonsági mentés visszaállítása", - "Restore": "Visszaállítás", - "RestartRequiredHelpTextWarning": "Újraindítás szükséges a hatálybalépéshez", + "Restore": "Visszaállít", + "RestartRequiredHelpTextWarning": "Újraindítás szükséges az életbe lépéshez", "RestartProwlarr": "{appName} Újraindítása", - "RestartNow": "Újraindítás Most", - "Restart": "Újraindítás", - "ResetAPIKey": "API Kulcs visszaállítása", + "RestartNow": "Újraindítás most", + "Restart": "Újrakezd", + "ResetAPIKey": "API Kulcs Visszaállítása", "Reset": "Visszaállítás", "RemovingTag": "Címke eltávolítása", - "RemoveFilter": "Szűrő törlése", - "RemovedFromTaskQueue": "Eltávolítva a feladatsorról", - "Reload": "Újratöltés", + "RemoveFilter": "Szűrő Eltávolítás", + "RemovedFromTaskQueue": "Eltávolítva a feladatsorból", + "Reload": "Újratölt", "ReleaseStatus": "Kiadás státusza", "ReleaseBranchCheckOfficialBranchMessage": "A(z) {0} nem érvényes {appName} frissítési ágazat, ezért nem kap frissítéseket", "RefreshMovie": "Film frissítése", "Refresh": "Frissítés", - "ReadTheWikiForMoreInformation": "Olvasd el a Wiki-t további információkért", + "ReadTheWikiForMoreInformation": "További információkért olvassa el a Wikit", "ProwlarrSupportsAnyIndexer": "A {appName} számos indexert támogat, minden olyan indexelő mellett, amely a Newznab / Torznab szabványt használja, valamint a 'Generic Newznab' (usenethez) vagy a 'Generic Torznab' (torrentekhez) használatával. Keresés és az alább felsorolt indexelők kiválasztása.", "ProwlarrSupportsAnyDownloadClient": "A {appName} minden olyan letöltési klienst támogat, amely a Newznab szabványt használja, valamint az alább felsorolt letöltési klienseket.", "Queue": "Várakozási sor", - "ProxyUsernameHelpText": "Csak akkor kell megadnod felhasználónevet és jelszót, ha szükséges. Egyébként hagyd üresen.", - "ProxyType": "Proxy Típusa", - "ProxyPasswordHelpText": "Csak akkor kell megadnod felhasználónevet és jelszót, ha szükséges. Egyébként hagyd üresen.", - "ProxyCheckResolveIpMessage": "Nem sikerült megoldani a konfigurált proxykiszolgáló IP-címét {0}", - "ProxyCheckFailedToTestMessage": "Proxy tesztelése sikertelen: {0}", - "ProxyCheckBadRequestMessage": "Proxy tesztelése sikertelen. Állapotkód: {0}", - "ProxyBypassFilterHelpText": "Használja elválasztóként a ',' és a '*' karaktereket, az aldomainek helyettesítőjeként", + "ProxyUsernameHelpText": "Csak akkor kell megadnia egy felhasználónevet és jelszót, ha szükséges. Ellenkező esetben hagyja üresen.", + "ProxyType": "Proxy típus", + "ProxyPasswordHelpText": "Csak akkor kell megadnia egy felhasználónevet és jelszót, ha szükséges. Ellenkező esetben hagyja üresen.", + "ProxyResolveIpHealthCheckMessage": "Nem sikerült megoldani a konfigurált proxykiszolgáló IP-címét {proxyHostName}", + "ProxyFailedToTestHealthCheckMessage": "Proxy tesztelése sikertelen: {url}", + "ProxyBadRequestHealthCheckMessage": "Proxy tesztelése sikertelen. Állapotkód: {statusCode}", + "ProxyBypassFilterHelpText": "Használja a ',' jelet elválasztóként és a '*' jelet. helyettesítő karakterként az aldomainekhez", "Proxy": "Proxy", "Protocol": "Protokoll", "Priority": "Prioritás", "PortNumber": "Port száma", "Port": "Port", - "PendingChangesStayReview": "Maradj, és tekintsd át a változásokat", - "PendingChangesMessage": "Nem mentett módosításaid vannak, biztosan el akarod hagyni ezt az oldalt?", - "PendingChangesDiscardChanges": "Változtatások törlése és kilépés", + "PendingChangesStayReview": "Maradjon és tekintse át a változtatásokat", + "PendingChangesMessage": "Vannak nem mentett módosításai. Biztosan elhagyja ezt az oldalt?", + "PendingChangesDiscardChanges": "Vesse el a változtatásokat, és lépjen ki", "Peers": "Peerek", "Password": "Jelszó", "PageSizeHelpText": "Az egyes oldalakon megjelenítendő elemek száma", "PageSize": "Oldal mérete", "PackageVersion": "Csomagverzió", - "Options": "Opciók", + "Options": "Lehetőségek", "OpenThisModal": "Nyissa meg ezt a modált", "OpenBrowserOnStart": "Indításkor nyissa meg a böngészőt", "OnHealthIssueHelpText": "Állapotprobléma", "Ok": "Ok", "OAuthPopupMessage": "A böngésződ blokkolja az előugró ablakokat", - "NoUpdatesAreAvailable": "Nincsenek elérhető frissítések", - "NoTagsHaveBeenAddedYet": "Még nem adtál hozzá címkéket", - "NoLogFiles": "Nincsen log fájl", - "NoLeaveIt": "Nem, hagyd így", - "NoChanges": "Nincsenek változások", - "NoChange": "Nincs változtatás", - "NoBackupsAreAvailable": "Nincs elérhető biztonsági mentés", + "NoUpdatesAreAvailable": "Nem érhetők el frissítések", + "NoTagsHaveBeenAddedYet": "Még nem adtak hozzá címkéket", + "NoLogFiles": "Nincsenek naplófájlok", + "NoLeaveIt": "Nem, Hagyd", + "NoChanges": "Nincs változás", + "NoChange": "Nincs változás", + "NoBackupsAreAvailable": "Nincsenek biztonsági mentések", "New": "Új", "NetCore": ".NET", "Name": "Név", "MovieIndexScrollTop": "Film Index: Görgess fel", "MovieIndexScrollBottom": "Film Index: Görgess le", - "MoreInfo": "Több Információ", + "MoreInfo": "Több információ", "Mode": "Mód", "MIA": "MIA", "Message": "Üzenet", - "Mechanism": "Mechanizmus", - "Manual": "Manuális", - "MaintenanceRelease": "Karbantartási frissítés: hibajavítások és egyéb fejlesztések. További részletekért lásd: Github Commit History", - "Logs": "Logok", - "LogLevelTraceHelpTextWarning": "A nyomkövetést csak ideiglenesen szabad engedélyezni", - "LogLevel": "Log Szint", + "Mechanism": "Gépezet", + "Manual": "Kézi", + "MaintenanceRelease": "Karbantartási kiadás: hibajavítások és egyéb fejlesztések. További részletekért lásd: Github Commit History", + "Logs": "Naplók", + "LogLevelTraceHelpTextWarning": "A nyomkövetési naplózást csak ideiglenesen szabad engedélyezni", + "LogLevel": "Napló szint", "Logging": "Loggolás", - "LogFiles": "Log Fájlok", + "LogFiles": "Naplófájlok", "Level": "Szint", "LaunchBrowserHelpText": " Nyisson meg egy böngészőt, és az alkalmazás indításakor lépjen a {appName} kezdőlapjára.", "LastWriteTime": "Utolsó írási idő", "Language": "Nyelv", - "KeyboardShortcuts": "Gyorsbillentyűk", + "KeyboardShortcuts": "Gyorsbillentyűket", "Interval": "Intervallum", "InteractiveSearch": "Interaktív Keresés", "Info": "Infó", - "IndexerStatusCheckSingleClientMessage": "Indexerek elérhetetlenek a következő hiba miatt: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Indexerek elérhetetlenek a következő hiba miatt: {indexerNames}", "Hostname": "Hosztnév", "Host": "Hoszt", - "Grabbed": "Megfogva", + "Grabbed": "Megragadta", "GeneralSettings": "Általános Beállítások", "General": "Általános", "UnableToLoadHistory": "Nem sikerült betölteni az előzményeket", "UnableToLoadGeneralSettings": "Nem sikerült betölteni az általános beállításokat", - "UnableToLoadDownloadClients": "Nem sikerült betölteni a letöltőkliens(eke)t", - "UnableToLoadBackups": "Biztonsági mentés(ek) betöltése sikertelen", + "DownloadClientsLoadError": "Nem sikerült betölteni a letöltőkliens(eke)t", + "BackupsLoadError": "Biztonsági mentés(ek) betöltése sikertelen", "UnableToAddANewNotificationPleaseTryAgain": "Nem lehet új értesítést hozzáadni, próbálkozz újra.", "UnableToAddANewIndexerPleaseTryAgain": "Nem lehet új indexert hozzáadni, próbálkozz újra.", "UnableToAddANewDownloadClientPleaseTryAgain": "Nem lehet új letöltőklienst hozzáadni, próbálkozz újra.", @@ -238,18 +238,18 @@ "Torrents": "Torrentek", "Title": "Cím", "Time": "Idő", - "TestAllClients": "Összes kliens tesztelése", - "TestAll": "Összes tesztelése", + "TestAllClients": "Minden ügyfél tesztelése", + "TestAll": "Összes Tesztelése", "Test": "Teszt", "Tasks": "Feladatok", - "TagsSettingsSummary": "Tekintse meg az összes címkét és azok használatát. A használatlan címkék eltávolíthatók", + "TagsSettingsSummary": "Tekintse meg az összes címkét és azok felhasználási módját. A fel nem használt címkék eltávolíthatók", "TagsHelpText": "Legalább egy megfelelő címkével rendelkező filmekre vonatkozik", "Tags": "Címkék", - "TagIsNotUsedAndCanBeDeleted": "A címkét nincs használatban, és törölhető", + "TagIsNotUsedAndCanBeDeleted": "A címke nincs használatban, törölhető", "TagCannotBeDeletedWhileInUse": "Használat közben nem törölhető", "TableOptionsColumnsMessage": "Válasszd ki, mely oszlopok legyenek láthatóak, és milyen sorrendben jelenjenek meg", - "SystemTimeCheckMessage": "A rendszeridő több mint 1 napja nem frissült. Előfordulhat, hogy az ütemezett feladatok az idő kijavításáig nem futnak megfelelően", - "IndexerStatusCheckAllClientMessage": "Az összes indexer elérhetetlen hiba miatt", + "SystemTimeHealthCheckMessage": "A rendszeridő több mint 1 napja nem frissült. Előfordulhat, hogy az ütemezett feladatok az idő kijavításáig nem futnak megfelelően", + "IndexerStatusAllUnavailableHealthCheckMessage": "Az összes indexer elérhetetlen hiba miatt", "Indexers": "Indexerek", "IndexerPriorityHelpText": "Indexelő prioritás 1-től (legmagasabb) 50-ig (legalacsonyabb). Alapértelmezés: 25.", "IndexerPriority": "Indexer Prioritása", @@ -257,9 +257,9 @@ "Indexer": "Indexelő", "IncludeHealthWarningsHelpText": "Tartalmazza a Állapot Figyelmeztetéseket", "IllRestartLater": "Később Újraindítom", - "IgnoredAddresses": "Ignorált Címek", - "YesCancel": "Igen, Mégsem", - "Warn": "Figyelmeztet", + "IgnoredAddresses": "Ignorált címek", + "YesCancel": "Igen, elvet", + "Warn": "Figyelmeztetés", "View": "Nézet", "Version": "Verzió", "Username": "Felhasználónév", @@ -270,22 +270,22 @@ "Uptime": "Üzemidő", "UpdateScriptPathHelpText": "Keresse meg az egyéni parancsfájl elérési útját, amely kibontott frissítési csomagot vesz fel, és kezeli a frissítési folyamat fennmaradó részét", "Updates": "Frissítések", - "UpdateMechanismHelpText": "Használja a {appName} beépített frissítőjét vagy egy szkriptet", - "UpdateCheckUINotWritableMessage": "Nem lehet telepíteni a frissítést, mert a(z) „{0}” felhasználói felület mappát nem írhatja a „{1}” felhasználó.", - "UpdateCheckStartupTranslocationMessage": "Nem lehet telepíteni a frissítést, mert a (z) „{0}” indítási mappa az Alkalmazások Transzlokációs mappájában található.", - "UpdateCheckStartupNotWritableMessage": "A frissítés nem telepíthető, mert a (z) „{0}” indítási mappát a „{1}” felhasználó nem írhatja.", + "UpdateMechanismHelpText": "Használja a {appName} beépített frissítőjét vagy szkriptjét", + "UpdateUiNotWritableHealthCheckMessage": "Nem lehet telepíteni a frissítést, mert a(z) „{uiFolder}” felhasználói felület mappát nem írhatja a „{userName}” felhasználó.", + "UpdateStartupTranslocationHealthCheckMessage": "Nem lehet telepíteni a frissítést, mert a (z) „{startupFolder}” indítási mappa az Alkalmazások Transzlokációs mappájában található.", + "UpdateStartupNotWritableHealthCheckMessage": "A frissítés nem telepíthető, mert a (z) „{startupFolder}” indítási mappát a „{userName}” felhasználó nem írhatja.", "UpdateAutomaticallyHelpText": "A frissítések automatikus letöltése és telepítése. A Rendszer: Frissítések alkalmazásból továbbra is telepíteni tudja", "UnselectAll": "Minden kijelölés megszüntetése", "UnsavedChanges": "Nem mentett változások", "UnableToLoadUISettings": "Nem sikerült betölteni a felhasználói felület beállításait", "UnableToLoadTags": "Nem sikerült betölteni a címkéket", "UnableToLoadNotifications": "Nem sikerült betölteni az Értesítéseket", - "TableOptions": "Táblázat beállításai", - "ShowSearch": "Keresés(ek) megjelenítése", + "TableOptions": "Táblázat Beállítások", + "ShowSearch": "Keresés mutatása", "SetTags": "Címkék beállítása", - "NotificationTriggers": "Értesítés(ek) kiváltója", - "IndexerLongTermStatusCheckSingleClientMessage": "Az összes indexer elérhetetlen több mint 6 órája, meghibásodás miatt: {0}", - "IndexerLongTermStatusCheckAllClientMessage": "Az összes indexer elérhetetlen több mint 6 órája, meghibásodás miatt", + "NotificationTriggers": "Értesítési triggerek", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Az összes indexer elérhetetlen több mint 6 órája, meghibásodás miatt: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Az összes indexer elérhetetlen több mint 6 órája, meghibásodás miatt", "SettingsLogSql": "SQL naplózás", "IndexerRss": "Indexer Rss", "IndexerAuth": "Indexelő auth", @@ -320,19 +320,19 @@ "AddNewIndexer": "Új indexelő hozzáadása", "AddedToDownloadClient": "Kiadás hozzáadva a klienshez", "EnableRssHelpText": "RSS Engedélyezése az Indexerekhez", - "EnableRss": "RSS Engedélyezése", + "EnableRss": "RSS Aktiválás", "Wiki": "Wiki", - "RSS": "RSS", + "Rss": "RSS", "RedirectHelpText": "Átirányítja a bejövő letöltési kérelmet az indexelő számára, és közvetlenül adja át a fájlt ahelyett, hogy a kérést a {appName}-en keresztül proxyba tenné", "Redirect": "Átirányítás", "Reddit": "Reddit", "HomePage": "Kezdőlap", - "FeatureRequests": "Funkció kérése", + "FeatureRequests": "Funkciókérés", "Discord": "Discord", "AppProfileSelectHelpText": "Az alkalmazásprofilok az RSS vezérlésére szolgálnak, Automatikus keresés és Interaktív keresés beállításai az alkalmazás szinkronizálásakor", "UnableToAddANewApplicationPleaseTryAgain": "Nem lehet új alkalmazást hozzáadni, próbálkozzon újra.", "UnableToAddANewAppProfilePleaseTryAgain": "Nem lehet új alkalmazásprofilt hozzáadni, próbálkozzon újra.", - "AddDownloadClient": "Letöltőkliens hozzáadása", + "AddDownloadClient": "Letöltési kliens hozzáadása", "Applications": "Alkalmazások", "AppProfileInUse": "Használatban lévő alkalmazásprofil", "Apps": "Appok", @@ -347,7 +347,7 @@ "Encoding": "Kódolás", "Grabs": "Megfogások", "Id": "Azonosító", - "NotificationTriggersHelpText": "Válaszd ki, hogy mely események indítsák el ezt az értesítést", + "NotificationTriggersHelpText": "Válassza ki, hogy mely események váltsák ki ezt az értesítést", "Presets": "Előbeállítások", "Privacy": "Titkosítás", "Query": "Lekérdezés", @@ -366,16 +366,16 @@ "DeleteIndexerProxy": "Indexer Proxy törlése", "DeleteIndexerProxyMessageText": "Biztosan törlöd a(z) „{0}” proxyt?", "IndexerProxies": "Indexer Proxy(k)", - "IndexerProxyStatusCheckAllClientMessage": "Az összes Proxy elérhetetlen, hiba miatt", - "IndexerProxyStatusCheckSingleClientMessage": "Proxyk elérhetetlenek az alábbi hibák miatt: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Az összes Proxy elérhetetlen, hiba miatt", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxyk elérhetetlenek az alábbi hibák miatt: {indexerProxyNames}", "IndexerTagsHelpText": "Címkék segítségével megadhatod az indexer proxykat amelyekkel az indexer szinkronizálva van, vagy egyszerűen az indexelők rendszerezéséhez.", "UnableToLoadIndexerProxies": "Nem lehet betölteni az Indexer Proxyt", "AddIndexerProxy": "Indexer Proxy hozzáadása", "IndexerSettingsSummary": "Konfigurálja a különböző globális indexer beállításokat, beleértve a proxykat is.", "Notifications": "Értesítések", "UnableToAddANewIndexerProxyPleaseTryAgain": "Nem lehet új Indexer Proxyt hozzáadni, próbálja újra.", - "IndexerVipCheckExpiredClientMessage": "Az Indexer VIP előnyei lejártak: {0}", - "IndexerVipCheckExpiringClientMessage": "Az Indexer VIP előnyei hamarosan lejárnak: {0}", + "IndexerVipExpiredHealthCheckMessage": "Az Indexer VIP előnyei lejártak: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Az Indexer VIP előnyei hamarosan lejárnak: {indexerNames}", "IndexerProxy": "Indexelő Proxy", "NoLinks": "Nincsenek Linkek", "Notification": "Értesítés", @@ -393,7 +393,7 @@ "HistoryCleanupDaysHelpTextWarning": "A kiválasztott napszámnál régebbi előzmények automatikusan törlődnek", "IndexerAlreadySetup": "Az indexelő legalább egy példánya már be van állítva", "IndexerInfo": "Indexer információ", - "IndexerNoDefCheckMessage": "Az indexereknek nincs definíciójuk, és nem működnek: {0}. Kérjük, távolítsa el és (vagy) adja hozzá újra a {appName}hoz", + "IndexerNoDefinitionCheckHealthCheckMessage": "Az indexereknek nincs definíciójuk, és nem működnek: {indexerNames}. Kérjük, távolítsa el és (vagy) adja hozzá újra a {appName}hoz", "MassEditor": "Tömeges szerkesztő", "OnApplicationUpdate": "Alkalmazásfrissítésről", "OnApplicationUpdateHelpText": "Alkalmazásfrissítésről", @@ -404,8 +404,8 @@ "Public": "Publikus", "QueryResults": "Lekérdezési eredmények", "SemiPrivate": "Fél-Privát", - "TestAllIndexers": "Indexerek tesztelése", - "UnableToLoadApplicationList": "Nem sikerült betölteni az alkalmazáslistát", + "TestAllIndexers": "Tesztelje az összes indexelőt", + "ApplicationsLoadError": "Nem sikerült betölteni az alkalmazáslistát", "Url": "URL", "UserAgentProvidedByTheAppThatCalledTheAPI": "Az API-t hívó alkalmazás biztosítja a User-Agent szolgáltatást", "Website": "Weboldal", @@ -444,8 +444,8 @@ "LastDuration": "Utolsó időtartam", "LastExecution": "Utolsó végrehajtás", "Parameters": "Paraméterek", - "Queued": "Sorba helyezve", - "Started": "Elkezdődött", + "Queued": "Sorban", + "Started": "Elindult", "NextExecution": "Következő végrehajtás", "ApplicationLongTermStatusCheckSingleClientMessage": "Az alkamazások elérhetetlenek több mint 6 órája az alábbi hiba miatt: {0}", "ApplicationLongTermStatusCheckAllClientMessage": "Az összes alkalmazás elérhetetlen több mint 6 órája meghibásodás miatt", @@ -454,54 +454,148 @@ "MappedCategories": "Térképezett kategóriák", "DownloadClientCategory": "Letöltési kliens kategória", "AuthenticationRequired": "Azonosítás szükséges", - "AuthenticationRequiredHelpText": "Módosítsd, hogy mely kérésekhez van szükség hitelesítésre. Ne változtasson, hacsak nem érti a kockázatokat.", - "AuthenticationRequiredWarning": "A hitelesítés nélküli távoli hozzáférés megakadályozása érdekében a {appName}nak mostantól engedélyezni kell a hitelesítést. Konfigurálja a hitelesítési módszert és a hitelesítési adatokat. Opcionálisan letilthatja a helyi címekről történő hitelesítést. További információkért tekintsd meg a GYIK-et.", - "TheLatestVersionIsAlreadyInstalled": "A {appName} legújabb verziója már telepítva van", + "AuthenticationRequiredHelpText": "Módosítsa, hogy mely kérésekhez van szükség hitelesítésre. Ne változtasson, hacsak nem érti a kockázatokat.", + "AuthenticationRequiredWarning": "A hitelesítés nélküli távoli hozzáférés megakadályozása érdekében a(z) {appName} alkalmazásnak engedélyeznie kell a hitelesítést. Opcionálisan letilthatja a helyi címekről történő hitelesítést.", + "OnLatestVersion": "A {appName} legújabb verziója már telepítva van", "Remove": "Eltávolítás", "Replace": "Kicserél", "ApplicationURL": "Alkalmazás URL", - "ApplicationUrlHelpText": "Az alkalmazás külső URL-címe, beleértve a http(s)://-t, a portot és az URL-alapot", + "ApplicationUrlHelpText": "Ennek az alkalmazásnak a külső URL-címe, beleértve a http-eket", "More": "Több", "Publisher": "Kiadó", "ApplyChanges": "Változások alkalmazása", - "ApplyTagsHelpTextReplace": "Csere: Cserélje ki a címkéket a beírt címkékre (az összes címke törléséhez ne adjon meg címkéket)", + "ApplyTagsHelpTextReplace": "Csere: Cserélje ki a címkéket a megadott címkékkel (az összes címke törléséhez ne írjon be címkéket)", "EditSelectedIndexers": "Kiválasztott indexelők szerkesztése", - "ApplyTagsHelpTextAdd": "Hozzáadás: Új címkék hozzáadása a meglévő címkékhez", + "ApplyTagsHelpTextAdd": "Hozzáadás: Adja hozzá a címkéket a meglévő címkék listájához", "ApplyTagsHelpTextHowToApplyApplications": "Hogyan adjunk hozzá címkéket a kiválasztott filmhez", - "ApplyTagsHelpTextHowToApplyIndexers": "Hogyan adjunk hozzá címkéket a kiválasztott filmhez", + "ApplyTagsHelpTextHowToApplyIndexers": "Címkék alkalmazása a kiválasztott indexelőkre", "ApplyTagsHelpTextRemove": "Eltávolítás: Távolítsa el a beírt címkéket", - "CountIndexersSelected": "{0} Indexelő(k) kiválasztva", - "DeleteSelectedDownloadClients": "Letöltőkliens Törlése", + "CountIndexersSelected": "{count} indexelő(k) kiválasztva", + "DeleteSelectedDownloadClients": "Letöltési kliens(ek) törlése", "DownloadClientPriorityHelpText": "Priorizálj több letöltési klienst. A Round-Robint azonos prioritású letöltőkliensek használják.", - "EditSelectedDownloadClients": "Kiválasztott letöltési kliensek szerkesztése", + "EditSelectedDownloadClients": "Kijelölt letöltési kliensek szerkesztése", "Label": "Címke", "SelectIndexers": "Indexelők keresése", - "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {0} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", + "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {length} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", "Episode": "Epizód", "Genre": "Műfajok", "Theme": "Téma", "Track": "Dal", - "UpdateAvailable": "Új frissítés elérhető", + "UpdateAvailableHealthCheckMessage": "Új frissítés elérhető", "Year": "Év", "Book": "Könyv", "Season": "Évad", "Album": "Album", "Artist": "Előadó", "Author": "Szerző", - "ConnectionLostReconnect": "A Radarr megpróbál automatikusan csatlakozni, vagy kattints a frissítés gombra.", + "ConnectionLostReconnect": "A(z) {appName} automatikusan megpróbál csatlakozni, vagy kattintson az újratöltés gombra lent.", "DeleteAppProfileMessageText": "Biztosan törli a {0} minőségi profilt?", - "RecentChanges": "Friss változtatások", + "RecentChanges": "Friss változások", "WhatsNew": "Mi az újdonság?", - "ConnectionLostToBackend": "A Radarr elvesztette kapcsolatát a háttérrendszerrel, a funkciók helyreállításához frissíts.", + "ConnectionLostToBackend": "A(z) {appName} megszakadt a kapcsolat a háttérrendszerrel, ezért újra kell tölteni a működés visszaállításához.", "minutes": "percek", - "AddConnection": "Gyűjtemény módosítása", - "NotificationStatusAllClientHealthCheckMessage": "Összes alkalmazás elérhetetlen hiba miatt", - "NotificationStatusSingleClientHealthCheckMessage": "Az alkalmazás nem áll rendelkezésre az alábbi hibák miatt: {0}", - "AuthBasic": "Alap (Böngésző felugró ablak)", - "AuthForm": "Felhasználó (Bejelentkezési oldal)", - "DisabledForLocalAddresses": "Letiltva a helyi címeknél", - "None": "Nincs", - "ResetAPIKeyMessageText": "Biztos hogy vissza szeretnéd állítani az API-Kulcsod?", + "AddConnection": "Csatlakozás hozzáadása", + "NotificationStatusAllClientHealthCheckMessage": "Az összes értesítés nem érhető el hibák miatt", + "NotificationStatusSingleClientHealthCheckMessage": "Az alkalmazás nem áll rendelkezésre az alábbi hibák miatt: {notificationNames}", + "AuthBasic": "Alap (böngésző előugró ablak)", + "AuthForm": "Űrlapok (bejelentkezési oldal)", + "DisabledForLocalAddresses": "Helyi címeknél letiltva", + "None": "Egyik sem", + "ResetAPIKeyMessageText": "Biztosan visszaállítja API-kulcsát?", "AuthenticationRequiredPasswordHelpTextWarning": "Adjon meg új jelszót", - "AuthenticationRequiredUsernameHelpTextWarning": "Adjon meg új felhasználónevet" + "AuthenticationRequiredUsernameHelpTextWarning": "Adjon meg új felhasználónevet", + "AppUpdated": "{appName} frissítve", + "AddApplication": "Alkalmazás hozzáadása", + "AddCustomFilter": "Egyéni szűrő hozzáadása", + "Clone": "Klón", + "Implementation": "Végrehajtás", + "AddConnectionImplementation": "Csatlakozás hozzáadása - {implementationName}", + "AddDownloadClientImplementation": "Letöltési kliens hozzáadása – {implementationName}", + "AddIndexerImplementation": "Indexelő hozzáadása - {implementationName}", + "ActiveApps": "Aktív alkalmazások", + "ActiveIndexers": "Indexerek", + "AuthenticationMethod": "Hitelesítési Módszer", + "AuthenticationMethodHelpTextWarning": "Kérjük, válasszon érvényes hitelesítési módot", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Erősítsd meg az új jelszót", + "DefaultNameCopiedProfile": "{name} - Másolat", + "EditConnectionImplementation": "Kapcsolat szerkesztése - {implementationName}", + "EditIndexerProxyImplementation": "Indexelő hozzáadása - {megvalósítás neve}", + "AddApplicationImplementation": "Csatlakozás hozzáadása - {implementationName}", + "AddIndexerProxyImplementation": "Indexelő hozzáadása - {implementationName}", + "EditIndexerImplementation": "Indexelő szerkesztése – {implementationName}", + "EditApplicationImplementation": "Csatlakozás hozzáadása - {megvalósítás neve}", + "EditDownloadClientImplementation": "Letöltési kliens szerkesztése – {implementationName}", + "NoIndexersFound": "Nem található indexelő", + "NoDownloadClientsFound": "Nem találhatók letöltő kliensek", + "AdvancedSettingsShownClickToHide": "Haladó beállítások megjelenítve, kattints az elrejtéshez", + "AdvancedSettingsHiddenClickToShow": "Haladó beállítások rejtve, kattints a megjelenítéshez", + "AppUpdatedVersion": "{appName} frissítve lett `{version}` verzióra, ahhoz hogy a legutóbbi változtatások életbelépjenek, töltsd újra a {appName}-t", + "AddCategory": "Kategória hozzáadása", + "External": "Külső", + "NoHistoryFound": "Nem található előzmény", + "InvalidUILanguage": "A felhasználói felület érvénytelen nyelvre van állítva, javítsa ki, és mentse el a beállításait", + "ManageDownloadClients": "Letöltési kliensek kezelése", + "DownloadClientAriaSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához", + "DeleteSelectedIndexersMessageText": "Biztosan törölni szeretne {count} kiválasztott indexelőt?", + "DownloadClientQbittorrentSettingsContentLayout": "Tartalom elrendezése", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Függetlenül attól, hogy a qBittorrent konfigurált tartalomelrendezését használja, az eredeti elrendezést a torrentből, vagy mindig hozzon létre egy almappát (qBittorrent 4.3.2)", + "DeleteSelectedDownloadClientsMessageText": "Biztosan törölni szeretné a kiválasztott {count} letöltési klienst?", + "CountDownloadClientsSelected": "{count} letöltési kliens kiválasztva", + "StopSelecting": "Kiválasztás leállítása", + "days": "napok", + "DeleteSelectedApplicationsMessageText": "Biztosan törölni szeretne {count} kiválasztott importlistát?", + "CountApplicationsSelected": "{count} Gyűjtemény(ek) kiválasztva", + "ManageClients": "Ügyfelek kezelése", + "IndexerDownloadClientHealthCheckMessage": "Indexelők érvénytelen letöltési kliensekkel: {indexerNames}.", + "PasswordConfirmation": "Jelszó megerősítése", + "SecretToken": "Titkos token", + "UseSsl": "SSL használata", + "Donate": "Adományoz", + "IndexerBeyondHDSettingsSearchTypes": "Keresés típusa", + "Mixed": "Mixed", + "TorrentBlackholeSaveMagnetFiles": "Magnet fájlok mentése", + "DownloadClientSettingsInitialState": "Kezdeti állapot", + "DownloadClientSettingsInitialStateHelpText": "A torrentek kezdeti állapota hozzáadva a következőhöz {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához", + "IndexerHDBitsSettingsCodecs": "Kodek", + "IndexerHDBitsSettingsMediums": "Közepes", + "BlackholeFolderHelpText": "Mappa, amelyben az {appName} tárolja az {extension} fájlt", + "Destination": "Rendeltetési hely", + "Directory": "Könyvtár", + "DownloadClientFloodSettingsAdditionalTags": "További címkék", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Hozzáadja a média tulajdonságait címkékként. A tippek példák.", + "TorrentBlackholeSaveMagnetFilesExtension": "Magnet Files kiterjesztés mentése", + "TorrentBlackholeTorrentFolder": "Torrent mappa", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Előtagot ad a deluge json URL-hez, lásd: {url}", + "CustomFilter": "Egyedi Szűrők", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Opcionális megosztott mappa a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Download Station hely használatához", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Aria2 hely használatához", + "DownloadClientSettingsUseSslHelpText": "Biztonságos kapcsolat használata, amikor a(z) {clientName} szolgáltatással csatlakozik", + "GrabRelease": "Release megragadása", + "ManualGrab": "Megfog", + "PrioritySettings": "Prioritás: {priority}", + "ProxyValidationBadRequest": "Proxy tesztelése sikertelen. Állapotkód: {statusCode}", + "Default": "Alapértelmezett", + "BuiltIn": "Beépített", + "Script": "Szkript", + "Any": "Bármi", + "PublishedDate": "Közzététel dátuma", + "Redirected": "Átirányítás", + "AllSearchResultsHiddenByFilter": "Az alkalmazott szűrők miatt, az összes keresési eredmény rejtve marad", + "HealthMessagesInfoBox": "Az állapotfelmérés okáról további információkat találhat, ha a sor végén található wikilinkre (könyv ikonra) kattint, vagy megnézi [logs] ({link}). Ha nehézségei vannak ezen üzenetek értelmezése során, forduljon ügyfélszolgálatunkhoz az alábbi linkeken.", + "AptUpdater": "A frissítés telepítéséhez használja az apt-t", + "DockerUpdater": "Frissítse a docker-tárolót a frissítés fogadásához", + "Download": "Letöltés", + "ErrorRestoringBackup": "Hiba a biztonsági mentés visszaállításakor", + "ExternalUpdater": "A {appName} egy külső frissítési mechanizmus használatára van konfigurálva", + "FailedToFetchUpdates": "Nem sikerült lekérni a frissítéseket", + "LogFilesLocation": "A naplófájlok itt találhatók: {location}", + "Logout": "Kijelentkezés", + "NoEventsFound": "Nem található események", + "RestartReloadNote": "Megjegyzés: A {appName} automatikusan újraindítja és újratölti a felületet a visszaállítási folyamatban.", + "UpdateAppDirectlyLoadError": "Nem lehetséges közvetlenül frissíteni a {appName}-t", + "WouldYouLikeToRestoreBackup": "Szeretné visszaállítani a(z) „{name}” biztonsági másolatot?", + "InstallLatest": "Legfrissebb telepítése", + "CurrentlyInstalled": "Jelenleg telepítve", + "PreviouslyInstalled": "Korábban telepítve" } diff --git a/src/NzbDrone.Core/Localization/Core/id.json b/src/NzbDrone.Core/Localization/Core/id.json index 8a538e620..6adb15878 100644 --- a/src/NzbDrone.Core/Localization/Core/id.json +++ b/src/NzbDrone.Core/Localization/Core/id.json @@ -1,7 +1,7 @@ { "ApplicationURL": "URL Aplikasi", "ApplicationUrlHelpText": "URL eksternal aplikasi termasuk http(s)://, port, dan dasar URL", - "AddDownloadClient": "Tambahkan Klien Pengunduhan", + "AddDownloadClient": "Tambahkan Download Client", "Added": "Ditambahkan", "AddIndexer": "Tambahkan Pengindeks", "Age": "Usia", @@ -19,22 +19,22 @@ "ChangeHasNotBeenSavedYet": "Perubahan belum disimpan", "EnableInteractiveSearch": "Aktifkan Penelusuran Interaktif", "Ended": "Berakhir", - "AuthenticationMethodHelpText": "Perlukan Nama Pengguna dan Sandi untuk mengakses Radarr", + "AuthenticationMethodHelpText": "Perlukan Nama Pengguna dan Sandi untuk mengakses {appName}", "Host": "Host", "Enable": "Aktif", "EnableRss": "Aktifkan RSS", "Indexer": "Pengindeks", "No": "Tidak", "Priority": "Prioritas", - "ProxyCheckBadRequestMessage": "Gagal menguji proxy. Kode Status: {0}", - "ProxyCheckFailedToTestMessage": "Gagal menguji proxy: {0}", + "ProxyBadRequestHealthCheckMessage": "Gagal menguji proxy. Kode Status: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Gagal menguji proxy: {url}", "Queue": "Antrean", "Backup": "Cadangan", "Backups": "Cadangan", "Categories": "Kategori", "Close": "Tutup", "ConnectionLost": "Koneksi Terputus", - "Connections": "Koleksi", + "Connections": "Koneksi", "Custom": "Khusus", "Dates": "Tanggal", "Delete": "Hapus", @@ -46,7 +46,7 @@ "Hostname": "Hostname", "Info": "Informasi", "Language": "Bahasa", - "LogFiles": "Berkas Log", + "LogFiles": "File Log", "NoChange": "Tidak Ada Perubahan", "NoChanges": "Tidak Ada Perubahan", "Queued": "Antrean", @@ -58,7 +58,36 @@ "Actions": "Tindakan", "AllIndexersHiddenDueToFilter": "Semua film disembunyikan karena penyaringan yang diterapkan.", "AnalyticsEnabledHelpText": "Kirimkan informasi penggunaan secara anonim ke server Radarr. Informasi tersebut mengandung browser kamu, halaman WebUI Radarr yang kamu gunakan, pelaporan masalah serta OS dan versi runtime. Kami akan memanfaatkan informasi ini untuk memprioritaskan fitur dan perbaikan bug.", - "ConnectionLostReconnect": "Radarr akan mencoba untuk menghubungi secara otomatis, atau klik muat ulang di bawah.", + "ConnectionLostReconnect": "{appName} akan mencoba untuk menghubungkan secara otomatis, atau silakan klik muat ulang di bawah.", "AuthBasic": "Dasar (Popup Browser)", - "AuthForm": "Formulir (Halaman Login)" + "AuthForm": "Formulir (Halaman Masuk)", + "Category": "Kategori", + "ApplyChanges": "Terapkan Perubahan", + "Today": "Hari Ini", + "Yesterday": "Kemarin", + "Search": "Cari", + "ConnectSettings": "Pengaturan Koneksi", + "Refresh": "Muat Ulang", + "ConnectionLostToBackend": "Koneksi {appName} telah terputus dari backend dan perlu dimuat ulang untuk dipulihkan.", + "Edit": "Edit", + "Files": "File", + "History": "Riwayat", + "AuthenticationMethod": "Metode Autentikasi", + "AuthenticationMethodHelpTextWarning": "Silakan pilih metode autentikasi yang sah", + "AuthenticationRequired": "Autentikasi Diperlukan", + "AuthenticationRequiredPasswordHelpTextWarning": "Masukkan sandi baru", + "AuthenticationRequiredUsernameHelpTextWarning": "Masukkan nama pengguna baru", + "AuthenticationRequiredWarning": "Untuk mencegah akses jarak jauh tanpa autentikasi, {appName} kini mewajibkan pengaktifkan autentikasi. Kamu dapat menonaktifkan autentikasi dari jaringan lokal.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Konfirmasi sandi baru", + "days": "hari", + "minutes": "menit", + "Link": "Tautan", + "Id": "ID", + "IndexerHDBitsSettingsCodecs": "Codec", + "ProxyValidationBadRequest": "Gagal menguji proxy. Kode Status: {statusCode}", + "AllSearchResultsHiddenByFilter": "Seluruh hasil disembunyikan karena penyaringan yang diterapkan", + "AptUpdater": "Gunakan apt untuk memasang pembaruan", + "Clone": "Tutup", + "EnableSSL": "Aktifkan RSS", + "CurrentlyInstalled": "Saat Ini Terpasang" } diff --git a/src/NzbDrone.Core/Localization/Core/is.json b/src/NzbDrone.Core/Localization/Core/is.json index 539af123b..e944179a8 100644 --- a/src/NzbDrone.Core/Localization/Core/is.json +++ b/src/NzbDrone.Core/Localization/Core/is.json @@ -23,8 +23,8 @@ "Exception": "Undantekning", "FocusSearchBox": "Fókus leitarreitur", "Grabbed": "Greip", - "IndexerProxyStatusCheckAllClientMessage": "Allir listar eru ekki tiltækir vegna bilana", - "IndexerProxyStatusCheckSingleClientMessage": "Listar ekki tiltækir vegna bilana: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Allir listar eru ekki tiltækir vegna bilana", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Listar ekki tiltækir vegna bilana: {indexerProxyNames}", "SSLCertPassword": "SSL vottorð lykilorð", "SSLCertPasswordHelpText": "Lykilorð fyrir PFX skrá", "SSLCertPath": "SSL vottunarleið", @@ -48,7 +48,7 @@ "Details": "Upplýsingar", "PortNumber": "Portnúmer", "Uptime": "Spenntur", - "SystemTimeCheckMessage": "Slökkt er á tíma kerfisins meira en 1 dag. Skipulögð verkefni ganga kannski ekki rétt fyrr en tíminn er leiðréttur", + "SystemTimeHealthCheckMessage": "Slökkt er á tíma kerfisins meira en 1 dag. Skipulögð verkefni ganga kannski ekki rétt fyrr en tíminn er leiðréttur", "TableOptions": "Borðvalkostir", "TableOptionsColumnsMessage": "Veldu hvaða dálkar eru sýnilegir og í hvaða röð þeir birtast", "TagCannotBeDeletedWhileInUse": "Ekki er hægt að eyða meðan hún er í notkun", @@ -64,8 +64,8 @@ "Torrents": "Flæði", "Type": "Tegund", "UnableToAddANewApplicationPleaseTryAgain": "Ekki er hægt að bæta við nýrri tilkynningu. Reyndu aftur.", - "UnableToLoadBackups": "Ekki er hægt að hlaða afrit", - "UnableToLoadDownloadClients": "Ekki er hægt að hlaða niður viðskiptavinum", + "BackupsLoadError": "Ekki er hægt að hlaða afrit", + "DownloadClientsLoadError": "Ekki er hægt að hlaða niður viðskiptavinum", "UnableToLoadGeneralSettings": "Ekki er hægt að hlaða almennar stillingar", "UnableToLoadHistory": "Ekki er hægt að hlaða sögu", "UnableToLoadNotifications": "Ekki er hægt að hlaða tilkynningar", @@ -104,7 +104,7 @@ "PackageVersion": "Pakki Útgáfa", "PageSize": "Stærð blaðsíðu", "PageSizeHelpText": "Fjöldi atriða til að sýna á hverri síðu", - "ProxyCheckResolveIpMessage": "Mistókst að leysa IP-tölu fyrir stillta proxy-gestgjafa {0}", + "ProxyResolveIpHealthCheckMessage": "Mistókst að leysa IP-tölu fyrir stillta proxy-gestgjafa {proxyHostName}", "ProxyPasswordHelpText": "Þú þarft aðeins að slá inn notendanafn og lykilorð ef þess er krafist. Láttu þá vera auða annars.", "ProxyType": "Umboðsmaður", "Queue": "Biðröð", @@ -127,9 +127,9 @@ "UnableToLoadUISettings": "Ekki er hægt að hlaða stillingar HÍ", "UnselectAll": "Afmarkaðu allt", "UpdateAutomaticallyHelpText": "Sjálfkrafa hlaða niður og setja upp uppfærslur. Þú munt samt geta sett upp úr System: Updates", - "UpdateCheckStartupNotWritableMessage": "Ekki er hægt að setja uppfærslu þar sem ræsimappan '{0}' er ekki skrifanleg af notandanum '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Ekki er hægt að setja uppfærslu vegna þess að ræsimappan '{0}' er í forritunarmöppu forrits.", - "UpdateCheckUINotWritableMessage": "Ekki er hægt að setja uppfærslu vegna þess að notendamöppan '{0}' er ekki skrifuð af notandanum '{1}'.", + "UpdateStartupNotWritableHealthCheckMessage": "Ekki er hægt að setja uppfærslu þar sem ræsimappan '{startupFolder}' er ekki skrifanleg af notandanum '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Ekki er hægt að setja uppfærslu vegna þess að ræsimappan '{startupFolder}' er í forritunarmöppu forrits.", + "UpdateUiNotWritableHealthCheckMessage": "Ekki er hægt að setja uppfærslu vegna þess að notendamöppan '{uiFolder}' er ekki skrifuð af notandanum '{userName}'.", "UpdateMechanismHelpText": "Notaðu innbyggða uppfærslu {appName} eða handrit", "Updates": "Uppfærslur", "UpdateScriptPathHelpText": "Leið að sérsniðnu handriti sem tekur útdreginn uppfærslupakka og annast afganginn af uppfærsluferlinu", @@ -199,7 +199,7 @@ "CouldNotConnectSignalR": "Gat ekki tengst SignalR, HÍ mun ekki uppfæra", "CustomFilters": "Sérsniðin síur", "Dates": "Dagsetningar", - "DBMigration": "DB fólksflutningar", + "DatabaseMigration": "DB fólksflutningar", "Delete": "Eyða", "DeleteApplicationMessageText": "Ertu viss um að þú viljir eyða tilkynningunni „{0}“?", "DeleteBackup": "Eyða afritun", @@ -211,15 +211,15 @@ "Discord": "Ósætti", "Docker": "Docker", "DownloadClients": "Sækja viðskiptavini", - "DownloadClientStatusCheckAllClientMessage": "Allir viðskiptavinir sem hlaða niður eru ekki tiltækir vegna bilana", - "DownloadClientStatusCheckSingleClientMessage": "Sæktu viðskiptavini sem eru ekki tiltækir vegna bilana: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "Allir viðskiptavinir sem hlaða niður eru ekki tiltækir vegna bilana", + "DownloadClientStatusSingleClientHealthCheckMessage": "Sæktu viðskiptavini sem eru ekki tiltækir vegna bilana: {downloadClientNames}", "EditIndexer": "Breyttu Indexer", "Enable": "Virkja", "EnableAutomaticSearch": "Virkja sjálfvirka leit", "EnableAutomaticSearchHelpText": "Verður notað þegar sjálfvirkar leitir eru framkvæmdar í HÍ eða af {appName}", "Filter": "Sía", "Fixed": "Fastur", - "HealthNoIssues": "Engin vandamál með stillingar þínar", + "NoIssuesWithYourConfiguration": "Engin vandamál með stillingar þínar", "Host": "Gestgjafi", "Hostname": "Gestgjafanafn", "IllRestartLater": "Ég byrja aftur seinna", @@ -227,12 +227,12 @@ "Indexer": "Indexer", "Info": "Upplýsingar", "IndexerFlags": "Indexer fánar", - "IndexerLongTermStatusCheckAllClientMessage": "Allir verðtryggingaraðilar eru ekki tiltækir vegna bilana í meira en 6 klukkustundir", - "IndexerLongTermStatusCheckSingleClientMessage": "Vísitölufólk er ekki tiltækt vegna bilana í meira en 6 klukkustundir: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Allir verðtryggingaraðilar eru ekki tiltækir vegna bilana í meira en 6 klukkustundir", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Vísitölufólk er ekki tiltækt vegna bilana í meira en 6 klukkustundir: {indexerNames}", "IndexerPriorityHelpText": "Forgangur flokkara frá 1 (Hæstur) til 50 (Lægstur). Sjálfgefið: 25.", "Indexers": "Vísitölufólk", - "IndexerStatusCheckAllClientMessage": "Allir verðtryggingaraðilar eru ekki tiltækir vegna bilana", - "IndexerStatusCheckSingleClientMessage": "Vísitölufólk er ekki tiltækt vegna bilana: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Allir verðtryggingaraðilar eru ekki tiltækir vegna bilana", + "IndexerStatusUnavailableHealthCheckMessage": "Vísitölufólk er ekki tiltækt vegna bilana: {indexerNames}", "NoLogFiles": "Engar logskrár", "Ok": "Allt í lagi", "OnHealthIssueHelpText": "Um heilbrigðismál", @@ -270,8 +270,8 @@ "Priority": "Forgangsröð", "Protocol": "Bókun", "ProxyBypassFilterHelpText": "Notaðu ',' sem aðskilnað og '*.' sem jókort fyrir undirlén", - "ProxyCheckBadRequestMessage": "Mistókst að prófa umboðsmann. Stöðukóði: {0}", - "ProxyCheckFailedToTestMessage": "Mistókst að prófa umboðsmann: {0}", + "ProxyBadRequestHealthCheckMessage": "Mistókst að prófa umboðsmann. Stöðukóði: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Mistókst að prófa umboðsmann: {url}", "ProxyUsernameHelpText": "Þú þarft aðeins að slá inn notendanafn og lykilorð ef þess er krafist. Láttu þá vera auða annars.", "ReleaseStatus": "Sleppa stöðu", "RemovedFromTaskQueue": "Fjarlægð úr verkröð", @@ -282,8 +282,8 @@ "Restore": "Endurheimta", "RestoreBackup": "Endurheimtu öryggisafrit", "Retention": "Varðveisla", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS er ekki studd með þessum flokkara", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS er ekki studd með þessum flokkara", "Save": "Vista", "SaveSettings": "Vista stillingar", "Scheduled": "Tímaáætlun", @@ -329,7 +329,7 @@ "NextExecution": "Næsta framkvæmd", "Remove": "Fjarlægðu", "Replace": "Skipta um", - "TheLatestVersionIsAlreadyInstalled": "Nýjasta útgáfan af {appName} er þegar uppsett", + "OnLatestVersion": "Nýjasta útgáfan af {appName} er þegar uppsett", "ApplyTagsHelpTextAdd": "Bæta við: Bættu merkjum við núverandi lista yfir merki", "ApplyTagsHelpTextHowToApplyApplications": "Hvernig á að setja merki á völdu kvikmyndirnar", "ApplyTagsHelpTextHowToApplyIndexers": "Hvernig á að setja merki á völdu kvikmyndirnar", @@ -347,11 +347,33 @@ "ConnectionLostReconnect": "Radarr mun reyna að tengjast sjálfkrafa eða þú getur smellt á endurhlaða hér að neðan.", "minutes": "Fundargerð", "NotificationStatusAllClientHealthCheckMessage": "Allir listar eru ekki tiltækir vegna bilana", - "NotificationStatusSingleClientHealthCheckMessage": "Listar ekki tiltækir vegna bilana: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Listar ekki tiltækir vegna bilana: {notificationNames}", "AuthBasic": "Grunn (sprettiglugga vafra)", "AuthForm": "Eyðublöð (Innskráningarsíða)", "DisabledForLocalAddresses": "Óvirkt vegna heimilisfanga", "None": "Enginn", "ResetAPIKeyMessageText": "Ertu viss um að þú viljir endurstilla API lykilinn þinn?", - "RestartProwlarr": "Endurræstu {appName}" + "RestartProwlarr": "Endurræstu {appName}", + "IndexerHDBitsSettingsMediums": "Miðlungs", + "CustomFilter": "Sérsniðin síur", + "ProxyValidationBadRequest": "Mistókst að prófa umboðsmann. Stöðukóði: {statusCode}", + "GrabRelease": "Grípa losun", + "BuiltIn": "Innbyggð", + "Script": "Handrit", + "PublishedDate": "Útgáfudagur", + "AllSearchResultsHiddenByFilter": "Allar niðurstöður eru faldar af beittu síunni", + "AptUpdater": "Notaðu apt til að setja uppfærsluna upp", + "DockerUpdater": "uppfærðu bryggjugáminn til að fá uppfærsluna", + "Download": "Sækja", + "ErrorRestoringBackup": "Villa við að endurheimta afrit", + "ExternalUpdater": "{appName} er stilltur til að nota ytri uppfærslu", + "RestartReloadNote": "Athugið: {appName} mun sjálfkrafa endurræsa og endurhlaða notendaviðmiðið meðan á endurreisnarferlinu stendur.", + "UpdateAppDirectlyLoadError": "Ekki er hægt að uppfæra {appName} beint,", + "NoEventsFound": "Engir viðburðir fundust", + "InstallLatest": "Settu upp nýjustu", + "Clone": "Lokaðu", + "CurrentlyInstalled": "Nú sett upp", + "Stats": "Staða", + "Mixed": "Fastur", + "Season": "Ástæða" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 91902561c..b9942e1b1 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -5,11 +5,11 @@ "TagsSettingsSummary": "Vedi tutte le etichette e come vengono utilizzate. Le etichette non utilizzate possono essere rimosse", "SetTags": "Imposta Etichette", "SelectAll": "Seleziona Tutto", - "Scheduled": "Programmato", + "Scheduled": "Pianificato", "ReleaseBranchCheckOfficialBranchMessage": "La versione {0} non è una versione valida per le release di {appName}, non riceverai aggiornamenti", - "ProxyCheckResolveIpMessage": "Impossibile risolvere l'indirizzo IP per l'Host Configurato del Proxy {0}", - "NoChanges": "Nessuna Modifica", - "NoChange": "Nessuna Modifica", + "ProxyResolveIpHealthCheckMessage": "Impossibile risolvere l'indirizzo IP per l'Host Configurato del Proxy {proxyHostName}", + "NoChanges": "Nessun Cambiamento", + "NoChange": "Nessun Cambio", "LastWriteTime": "Orario di Ultima Scrittura", "Indexer": "Indicizzatore", "HideAdvanced": "Nascondi Avanzate", @@ -17,13 +17,13 @@ "Grabbed": "Preso", "Clear": "Cancella", "AppDataLocationHealthCheckMessage": "L'aggiornamento non sarà possibile per evitare la cancellazione di AppData durante l'aggiornamento", - "Analytics": "Analitica", + "Analytics": "Statistiche", "Added": "Aggiunto", "About": "Info", "Updates": "Aggiornamenti", - "UpdateCheckUINotWritableMessage": "Impossibile installare l'aggiornamento perché l'utente '{1}' non ha i permessi di scrittura per la cartella dell'interfaccia '{0}'.", - "UpdateCheckStartupNotWritableMessage": "Impossibile installare l'aggiornamento perché l'utente '{1}' non ha i permessi di scrittura per la cartella di avvio '{0}'.", - "UpdateCheckStartupTranslocationMessage": "Impossibile installare l'aggiornamento perché la cartella '{0}' si trova in una cartella di \"App Translocation\".", + "UpdateUiNotWritableHealthCheckMessage": "Impossibile installare l'aggiornamento perché l'utente '{userName}' non ha i permessi di scrittura per la cartella dell'interfaccia '{uiFolder}'.", + "UpdateStartupNotWritableHealthCheckMessage": "Impossibile installare l'aggiornamento perché l'utente '{userName}' non ha i permessi di scrittura per la cartella di avvio '{startupFolder}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Impossibile installare l'aggiornamento perché la cartella '{startupFolder}' si trova in una cartella di \"App Translocation\".", "UI": "Interfaccia", "Tasks": "Attività", "Tags": "Etichette", @@ -35,23 +35,23 @@ "ShowAdvanced": "Mostra Avanzate", "Settings": "Impostazioni", "Security": "Sicurezza", - "Search": "Cerca", - "SaveChanges": "Salva Modifiche", + "Search": "Ricerca", + "SaveChanges": "Salva Cambiamenti", "RestoreBackup": "Ripristina Backup", "ReleaseStatus": "Stato Release", "Refresh": "Aggiorna", "Queue": "Coda", - "ProxyCheckFailedToTestMessage": "Test del proxy fallito: {0}", - "ProxyCheckBadRequestMessage": "Il test del proxy è fallito. Codice Stato: {0}", + "ProxyFailedToTestHealthCheckMessage": "Test del proxy fallito: {url}", + "ProxyBadRequestHealthCheckMessage": "Il test del proxy è fallito. Codice Stato: {statusCode}", "Proxy": "Proxy", "Protocol": "Protocollo", "Options": "Opzioni", - "MoreInfo": "Maggiori Info", + "MoreInfo": "Ulteriori Informazioni", "Logging": "Logging", "LogFiles": "File di Log", "Language": "Lingua", - "IndexerStatusCheckSingleClientMessage": "Indicizzatori non disponibili a causa di errori: {0}", - "IndexerStatusCheckAllClientMessage": "Nessun Indicizzatore disponibile a causa di errori", + "IndexerStatusUnavailableHealthCheckMessage": "Indicizzatori non disponibili a causa di errori: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Nessun Indicizzatore disponibile a causa di errori", "Indexers": "Indicizzatori", "Host": "Host", "History": "Storia", @@ -65,8 +65,8 @@ "EventType": "Tipo di Evento", "Events": "Eventi", "Edit": "Modifica", - "DownloadClientStatusCheckSingleClientMessage": "Client per il download non disponibili per errori: {0}", - "DownloadClientStatusCheckAllClientMessage": "Nessun client di download è disponibile a causa di errori", + "DownloadClientStatusSingleClientHealthCheckMessage": "Client per il download non disponibili per errori: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Nessun client di download è disponibile a causa di errori", "DownloadClientsSettingsSummary": "Configurazione dei client di download per l'integrazione nella ricerca di {appName}", "DownloadClients": "Clients di Download", "DownloadClient": "Client di Download", @@ -76,7 +76,7 @@ "Date": "Data", "CustomFilters": "Filtri Personalizzati", "Connect": "Notifiche", - "Connections": "Collegamenti", + "Connections": "Connessioni", "ConnectSettingsSummary": "Notifiche e script personalizzati", "BackupNow": "Esegui backup ora", "Backup": "Backup", @@ -84,39 +84,39 @@ "Actions": "Azioni", "Age": "Età", "Close": "Chiudi", - "CloneProfile": "Clona il Profilo", - "ClientPriority": "Priorità del Client", + "CloneProfile": "Copia Profilo", + "ClientPriority": "Priorità Client", "ChangeHasNotBeenSavedYet": "Il cambio non è stato ancora salvato", "CertificateValidationHelpText": "Cambia quanto è rigorosa la convalida del certificato HTTPS", "CertificateValidation": "Convalida del Certificato", "Cancel": "Annulla", "BypassProxyForLocalAddresses": "Evita il Proxy per gli Indirizzi Locali", - "Branch": "Ramo", - "BindAddressHelpText": "Indirizzo IP valido, localhost o '*' per tutte le interfacce di rete", - "BindAddress": "Indirizzo di Bind", - "Backups": "Backups", - "BackupRetentionHelpText": "I backup automatici più vecchi del periodo di conservazione verranno eliminati automaticamente", - "BackupIntervalHelpText": "Intervallo tra i backup automatici", + "Branch": "Branca", + "BindAddressHelpText": "Indirizzi IP validi, localhost o '*' per tutte le interfacce", + "BindAddress": "Indirizzo di Ascolto", + "Backups": "Backup", + "BackupRetentionHelpText": "I backup più vecchi del periodo specificato saranno cancellati automaticamente", + "BackupIntervalHelpText": "Intervallo fra i backup automatici", "BackupFolderHelpText": "I percorsi relativi saranno nella cartella AppData di {appName}", "Automatic": "Automatico", - "AuthenticationMethodHelpText": "Richiedi Nome Utente e Password per accedere a {appName}", + "AuthenticationMethodHelpText": "Utilizza nome utente e password per accedere a {appName}", "Authentication": "Autenticazione", "ApplyTags": "Applica Etichette", "Apply": "Applica", "AppDataDirectory": "Cartella AppData", "ApiKey": "Chiave API", - "AnalyticsEnabledHelpText": "Invia informazioni anonime sull'uso e sugli errori ai server di {appName}. Questo include informazioni sul tuo browser, quali pagine della WebUI di {appName} utilizzi, la segnalazione di errori così come il sistema operativo e la versione di runtime. Useremo queste informazioni per dare priorità alle funzionalità e alle correzioni dei bug.", + "AnalyticsEnabledHelpText": "Inviare informazioni anonime sull'utilizzo e sugli errori ai server di {appName}. Ciò include informazioni sul tuo browser, quali pagine dell'interfaccia di {appName} usi, la segnalazione di errori così come la versione del sistema operativo e del runtime. Utilizzeremo queste informazioni per dare priorità alle nuove funzioni e alle correzioni di bug.", "Warn": "Attenzione", "Type": "Tipo", "Title": "Titolo", - "Time": "Ora", - "TestAll": "Prova Tutti", - "Test": "Test", + "Time": "Orario", + "TestAll": "Prova Tutto", + "Test": "Prova", "TableOptionsColumnsMessage": "Scegli quali colonne rendere visibili ed il loro ordine", - "TableOptions": "Opzioni della tabella", - "SystemTimeCheckMessage": "L'orario di sistema è sbagliato di più di un giorno. Le attività pianificate potrebbero non essere eseguite correttamente fino alla correzione", + "TableOptions": "Opzioni Tabella", + "SystemTimeHealthCheckMessage": "L'orario di sistema è sbagliato di più di un giorno. Le attività pianificate potrebbero non essere eseguite correttamente fino alla correzione", "Source": "Fonte", - "Shutdown": "Spegni", + "Shutdown": "Spegnimento", "Seeders": "Seeders", "Save": "Salva", "Restart": "Riavvia", @@ -130,24 +130,24 @@ "Level": "Livello", "KeyboardShortcuts": "Scorciatoie Tastiera", "Info": "Info", - "HealthNoIssues": "La tua configurazione non presenta problemi", + "NoIssuesWithYourConfiguration": "La tua configurazione non presenta problemi", "Error": "Errore", "Enable": "Abilita", "DownloadClientSettings": "Impostazioni del Client di Download", "Docker": "Docker", "DeleteTag": "Elimina Etichetta", - "DeleteNotification": "Cancella la Notifica", - "DeleteDownloadClient": "Cancella il Client di Download", + "DeleteNotification": "Cancella Notifica", + "DeleteDownloadClient": "Cancella Client di Download", "DeleteBackup": "Cancella Backup", - "DBMigration": "Migrazione DB", - "ConnectSettings": "Impostazioni di Connessione", + "DatabaseMigration": "Migrazione Database", + "ConnectSettings": "Impostazioni Collegamento", "ConnectionLost": "Connessione Persa", "Component": "Componente", "Columns": "Colonne", - "DeleteBackupMessageText": "Sei sicuro di voler cancellare il backup '{0}'?", + "DeleteBackupMessageText": "Sei sicuro di voler eliminare il backup '{name}'?", "CancelPendingTask": "Sei sicuro di voler cancellare questa operazione in sospeso?", "BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno", - "BranchUpdate": "Ramo da usare per aggiornare {appName}", + "BranchUpdate": "Branca da usare per aggiornare {appName}", "AddingTag": "Aggiungendo etichetta", "Password": "Password", "OnHealthIssueHelpText": "Quando c'è un problema", @@ -162,41 +162,41 @@ "SettingsEnableColorImpairedMode": "Abilità la Modalità Daltonica", "SendAnonymousUsageData": "Invia dati anonimi sull'uso", "ScriptPath": "Percorso dello script", - "RSSIsNotSupportedWithThisIndexer": "RSS non è supportato con questo Indicizzatore", + "RssIsNotSupportedWithThisIndexer": "RSS non è supportato con questo indicizzatore", "Retention": "Ritenzione", "Result": "Risultato", "Restore": "Ripristina", "RestartRequiredHelpTextWarning": "Richiede il riavvio per avere effetto", "RestartProwlarr": "Riavvia {appName}", - "RestartNow": "Riavvia adesso", + "RestartNow": "Riavvia ora", "ResetAPIKey": "Resetta la Chiave API", - "Reset": "Resetta", + "Reset": "Reimposta", "RemovingTag": "Eliminando l'etichetta", "RemoveFilter": "Rimuovi filtro", "RemovedFromTaskQueue": "Rimosso dalla coda lavori", "RefreshMovie": "Aggiorna il Film", - "ReadTheWikiForMoreInformation": "Leggi la Wiki per maggiori informazioni", + "ReadTheWikiForMoreInformation": "Leggi la Wiki per più informazioni", "ProwlarrSupportsAnyIndexer": "{appName} supporta molti indicizzatori oltre a qualsiasi indicizzatore che utilizza lo standard Newznab/Torznab utilizzando \"Generic Newznab\" (per usenet) o \"Generic Torznab\" (per torrent). Cerca e seleziona il tuo indicizzatore da qua sotto.", "ProwlarrSupportsAnyDownloadClient": "{appName} supporta qualunque client di download elencato sotto.", "ProxyUsernameHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.", - "ProxyType": "Tipo di Proxy", + "ProxyType": "Tipo Proxy", "ProxyPasswordHelpText": "Devi inserire nome utente e password solo se richiesto. Altrimenti lascia vuoto.", - "ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come jolly per i sottodomini", - "PortNumber": "Numero di porta", + "ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come wildcard per i sottodomini", + "PortNumber": "Numero Porta", "Port": "Porta", - "PendingChangesStayReview": "Rimani e rivedi modifiche", - "PendingChangesMessage": "Hai cambiamenti non salvati, sicuro di voler abbandonare la pagina?", + "PendingChangesStayReview": "Rimani e rivedi i cambiamenti", + "PendingChangesMessage": "Hai dei cambiamenti non salvati, sei sicuro di volere lasciare questa pagina?", "PendingChangesDiscardChanges": "Abbandona le modifiche ed esci", "PageSizeHelpText": "Numero di voci da mostrare in ogni pagina", "PackageVersion": "Versione del Pacchetto", - "OpenBrowserOnStart": "Apri il browser all'avvio", + "OpenBrowserOnStart": "Apri browser all'avvio", "NoUpdatesAreAvailable": "Nessun aggiornamento disponibile", "NoTagsHaveBeenAddedYet": "Nessuna etichetta è ancora stata aggiunta", "NoLogFiles": "Nessun file di log", "NoLeaveIt": "No, Lascialo", - "NoBackupsAreAvailable": "Nessun Backup disponibile", + "NoBackupsAreAvailable": "Nessun backup disponibile", "New": "Nuovo", - "Mode": "Modo", + "Mode": "Modalità", "Mechanism": "Meccanismo", "Manual": "Manuale", "MaintenanceRelease": "Release di Manutenzione: correzione di bug e altri miglioramenti. Vedi la storia dei Commit su Github per maggiori dettagli", @@ -209,8 +209,7 @@ "IgnoredAddresses": "Indirizzi Ignorati", "GeneralSettings": "Impostazioni Generali", "ForMoreInformationOnTheIndividualDownloadClients": "Per più informazioni sui singoli client di download clicca sui pulsanti info.", - "Fixed": "Fissato", - "FilterPlaceHolder": "Cerca Indicizzatori", + "FilterPlaceHolder": "Cerca indicizzatori", "ExistingTag": "Etichetta esistente", "Exception": "Eccezione", "ErrorLoadingContents": "Errore nel caricare i contenuti", @@ -219,29 +218,29 @@ "EnableInteractiveSearch": "Abilita la Ricerca Interattiva", "EnableAutomaticSearchHelpText": "Sarà usata quando la ricerca automatica è eseguita dalla l'intrfaccia o da {appName}", "EnableAutomaticSearch": "Attiva la Ricerca Automatica", - "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{0}'?", - "DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{0}'?", - "DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{0}'?", - "BeforeUpdate": "Prima di aggiornare", + "DeleteTagMessageText": "Sei sicuro di voler eliminare l'etichetta '{label}'?", + "DeleteNotificationMessageText": "Sei sicuro di voler eliminare la notifica '{name}'?", + "DeleteDownloadClientMessageText": "Sei sicuro di voler eliminare il client di download '{name}'?", + "BeforeUpdate": "Prima dell'aggiornamento", "Usenet": "Usenet", "Uptime": "Tempo di attività", - "YesCancel": "Si, Cancella", + "YesCancel": "Sì, Cancella", "Version": "Versione", - "Username": "Nome utente", + "Username": "Nome Utente", "UseProxy": "Usa Proxy", "UrlBaseHelpText": "Per il supporto al reverse proxy, di default è vuoto", "URLBase": "Base Url", "UpdateScriptPathHelpText": "Percorso verso uno script personalizzato che prende un pacchetto di aggiornamento estratto e gestisce il resto del processo di aggiornamento", - "UpdateMechanismHelpText": "Usa l'aggiornamento integrato in {appName} o uno script", + "UpdateMechanismHelpText": "Usa il sistema di aggiornamento incorporato di {appName} o uno script", "UpdateAutomaticallyHelpText": "Scarica e installa automaticamente gli aggiornamenti. Sarai comunque in grado in installarli da Sistema: Aggiornamenti", - "UnsavedChanges": "Modifiche non salvate", + "UnsavedChanges": "Cambiamenti Non Salvati", "UnableToLoadUISettings": "Impossibile caricare le impostazioni interfaccia", "UnableToLoadTags": "Impossibile caricare le Etichette", "UnableToLoadNotifications": "Impossibile caricare le Notifiche", "UnableToLoadHistory": "Impossibile caricare la storia", "UnableToLoadGeneralSettings": "Impossibile caricare le impostazioni Generali", - "UnableToLoadDownloadClients": "Impossibile caricare i client di download", - "UnableToLoadBackups": "Impossibile caricare i backup", + "DownloadClientsLoadError": "Impossibile caricare i client di download", + "BackupsLoadError": "Impossibile caricare i backup", "UnableToAddANewNotificationPleaseTryAgain": "Impossibile aggiungere una nuova notifica, riprova.", "UnableToAddANewIndexerPleaseTryAgain": "Impossibile aggiungere un nuovo Indicizzatore, riprova.", "UnableToAddANewDownloadClientPleaseTryAgain": "Impossibile aggiungere un nuovo client di download, riprova.", @@ -255,7 +254,7 @@ "TagIsNotUsedAndCanBeDeleted": "L'etichetta non è in uso e può essere eliminata", "TagCannotBeDeletedWhileInUse": "Non può essere cancellato mentre è in uso", "SuggestTranslationChange": "Suggerisci un cambio nella traduzione", - "StartupDirectory": "Cartella di avvio", + "StartupDirectory": "Cartella di Avvio", "StartTypingOrSelectAPathBelow": "Comincia a digitare o seleziona un percorso sotto", "SSLPort": "Porta SSL", "SSLCertPathHelpText": "Percorso file pfx", @@ -276,7 +275,7 @@ "EditIndexer": "Modifica Indicizzatore", "Disabled": "Disabilitato", "AutomaticSearch": "Ricerca Automatica", - "AddIndexer": "Aggiungi Indicizzatore", + "AddIndexer": "Aggiungi Indexer", "SaveSettings": "Salva Impostazioni", "OpenThisModal": "Apri questa Modale", "MovieIndexScrollTop": "Indice film: scorri in alto", @@ -284,32 +283,31 @@ "FocusSearchBox": "Evidenzia casella di ricerca", "CloseCurrentModal": "Chiudi la Modale Attuale", "AcceptConfirmationModal": "Accetta Conferma Modale", - "IndexerLongTermStatusCheckSingleClientMessage": "Alcuni Indicizzatori non sono disponibili da più di 6 ore a causa di errori: {0}", - "IndexerLongTermStatusCheckAllClientMessage": "Nessun Indicizzatore è disponibile da più di 6 ore a causa di errori", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Alcuni Indicizzatori non sono disponibili da più di 6 ore a causa di errori: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Nessun Indicizzatore è disponibile da più di 6 ore a causa di errori", "Description": "Descrizione", "Add": "Aggiungi", "Enabled": "Abilitato", "Encoding": "Codifica", "EnableIndexer": "Abilita Indicizzatore", - "AddNewIndexer": "Aggiungi nuovo Indicizzatore", + "AddNewIndexer": "Aggiungi nuovo Indexer", "IndexerAuth": "Autenticazione dell'Indicizzatore", - "AddDownloadClient": "Aggiungi Downloader", + "AddDownloadClient": "Aggiungi Client di Download", "Category": "Categoria", "ClearHistory": "Cancella cronologia", "ClearHistoryMessageText": "Sei sicuro di voler cancellare tutta la cronologia di {appName}?", - "Discord": "Discord", "Donations": "Donazioni", "EnableRssHelpText": "Abilita feed RSS per l'Indicizzatore", "HomePage": "Pagina Iniziale", - "Id": "Id", + "Id": "ID", "IndexerHealthCheckNoIndexers": "Nessun Indicizzatore abilitato, {appName} non restituirà risultati di ricerca", "EnableRss": "Abilita RSS", "NoLinks": "Nessun Collegamento", - "RSS": "RSS", + "Rss": "RSS", "Wiki": "Wiki", - "AllIndexersHiddenDueToFilter": "Tutti gli Indicizzatori sono nascosti a causa del filtro applicato.", - "DeleteApplicationMessageText": "Sei sicuro di voler eliminare l'applicazione '{0}'?", - "DeleteIndexerProxyMessageText": "Sei sicuro di voler eliminare il proxy '{0}'?", + "AllIndexersHiddenDueToFilter": "Tutti gli Indexer 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", "SearchIndexers": "Cerca Indicizzatori", "UnableToAddANewIndexerProxyPleaseTryAgain": "Impossibile aggiungere un nuovo proxy per l'Indicizzatore, riprova.", @@ -322,8 +320,8 @@ "Custom": "Personalizzato", "FeatureRequests": "Richieste di funzionalità", "Grabs": "Prendere", - "IndexerProxyStatusCheckAllClientMessage": "Tutti i Proxy non sono disponibili a causa di errori", - "IndexerProxyStatusCheckSingleClientMessage": "Proxy non disponibili a causa di errori: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tutti i Proxy non sono disponibili a causa di errori", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxy non disponibili a causa di errori: {indexerProxyNames}", "Reddit": "Reddit", "Torrent": "Torrent", "UnableToAddANewApplicationPleaseTryAgain": "Impossibile aggiungere una nuova applicazione, riprova.", @@ -343,8 +341,8 @@ "MappedDrivesRunningAsService": "Le unità di rete mappate non sono disponibili eseguendo come servizio di Windows. Vedere le FAQ per maggiori informazioni", "No": "No", "UnableToLoadIndexers": "Impossibile caricare gli Indicizzatori", - "Yes": "Si", - "AddIndexerProxy": "Aggiungi proxy dell'Indicizzatore", + "Yes": "Sì", + "AddIndexerProxy": "Aggiungi proxy dell'Indexer", "AudioSearch": "Ricerca Audio", "BookSearch": "Ricerca Libri", "AddRemoveOnly": "Solo Aggiunta e Rimozione", @@ -366,15 +364,15 @@ "Apps": "Le App", "DevelopmentSettings": "Impostazioni di Sviluppo", "RedirectHelpText": "Reindirizza le richieste di download per l'Indicizzatore e passa il prelievo direttamente invece di inoltrare la richiesta tramite {appName}", - "IndexerVipCheckExpiredClientMessage": "I benefici VIP dell'Indicizzatore sono scaduti: {0}", - "IndexerVipCheckExpiringClientMessage": "I benefici VIP dell'Indicizzatore scadranno a breve: {0}", + "IndexerVipExpiredHealthCheckMessage": "I benefici VIP dell'Indicizzatore sono scaduti: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "I benefici VIP dell'Indicizzatore scadranno a breve: {indexerNames}", "IndexerProxies": "Proxy degli Indicizzatori", "Stats": "Statistiche", "SyncAppIndexers": "Sincronizza tutti gli indicizzatori", "SyncLevel": "Livello Sincronizzazione", "IndexerProxy": "Proxy dell'Indicizzatore", "Proxies": "Proxy", - "UnableToLoadApplicationList": "Impossibile careicare la lista applicazioni", + "ApplicationsLoadError": "Impossibile careicare la lista applicazioni", "Website": "Sito", "Privacy": "Privacy", "SettingsIndexerLogging": "Logging Migliorato dell'Indicizzatore", @@ -386,7 +384,7 @@ "IndexerDetails": "Dettagli dell'Indicizzatore", "IndexerInfo": "Info sull'Indicizzatore", "IndexerName": "Nome dell'Indicizzatore", - "IndexerNoDefCheckMessage": "Gli indicizzatori non hanno una definizione e non funzioneranno: {0}. Si prega di rimuoverli e/o di riaggiungerli a {appName}", + "IndexerNoDefinitionCheckHealthCheckMessage": "Gli indicizzatori non hanno una definizione e non funzioneranno: {indexerNames}. Si prega di rimuoverli e/o di riaggiungerli a {appName}", "HistoryCleanup": "Pulizia della Cronologia", "IndexerRss": "RSS dell'Indicizzatore", "IndexerSite": "Sito dell'Indicizzatore", @@ -435,10 +433,10 @@ "MinimumSeeders": "Seeder Minimi", "InstanceName": "Nome Istanza", "InstanceNameHelpText": "Nome istanza nella scheda e per il nome dell'app nel Syslog", - "ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il suo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da {0}", + "ThemeHelpText": "Cambia il Tema dell'interfaccia dell’applicazione, il Tema 'Auto' userà il tuo Tema di Sistema per impostare la modalità Chiara o Scura. Ispirato da {inspiredBy}.", "LastDuration": "Ultima Durata", "LastExecution": "Ultima esecuzione", - "Queued": "In coda", + "Queued": "In Coda", "ApplicationLongTermStatusCheckAllClientMessage": "Tutte le app non disponibili da almeno 6 ore a causa di errori", "ApplicationLongTermStatusCheckSingleClientMessage": "Alcune app non sono disponibili da almeno 6 ore a causa di errori: {0}", "Duration": "Durata", @@ -451,14 +449,14 @@ "Started": "Iniziato", "AreYouSureYouWantToDeleteCategory": "Vuoi davvero cancellare la categoria mappata?", "AuthenticationRequired": "Autenticazione richiesta", - "AuthenticationRequiredHelpText": "Cambia a quali richieste l'autenticazione verrà richiesta. Non cambiare se non comprendi i rischi.", - "AuthenticationRequiredWarning": "Per impedire accessi remoti senza autenticazione, {appName} ora richiede che l'autenticazione venga attivata. Puoi disattivare l'autenticazione per indirizzi privati.", + "AuthenticationRequiredHelpText": "Cambia a quali richieste l'autenticazione verrà chiesta. Non cambiare se non comprendi i rischi.", + "AuthenticationRequiredWarning": "Per prevenire accessi remoti non autorizzati, {appName} da ora richiede che l'autenticazione sia abilitata. Opzionalmente puoi disabilitare l'autenticazione per gli indirizzi di rete locali.", "DeleteClientCategory": "Cancella categoria del client di download", "DownloadClientCategory": "Categoria client di download", "MappedCategories": "Categorie mappate", "Remove": "Rimuovi", "Replace": "Sostituire", - "TheLatestVersionIsAlreadyInstalled": "L'ultima versione di {appName} è già installata", + "OnLatestVersion": "L'ultima versione di {appName} è già installata", "ApplicationURL": "URL Applicazione", "ApplicationUrlHelpText": "L'URL esterno di questa applicazione, incluso http(s)://, porta e URL base", "Episode": "Episodio", @@ -467,9 +465,9 @@ "ApplyTagsHelpTextAdd": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette", "ApplyTagsHelpTextHowToApplyApplications": "Come applicare etichette agli autori selezionati", "ApplyTagsHelpTextHowToApplyIndexers": "Come applicare etichette agli indicizzatori selezionati", - "CountIndexersSelected": "{0} indicizzatore(i) selezionato(i)", - "DeleteSelectedApplicationsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?", - "DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?", + "CountIndexersSelected": "{count} indicizzatore(i) selezionato(i)", + "DeleteSelectedApplicationsMessageText": "Sei sicuro di voler eliminare {count} applicazione(i) selezionata(e)?", + "DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare i '{count}' client di download selezionato/i?", "SelectIndexers": "Cerca Indicizzatori", "Track": "Traccia", "Book": "Libro", @@ -478,29 +476,190 @@ "ApplyTagsHelpTextReplace": "Sostituire: Sostituisce le etichette con quelle inserite (non inserire nessuna etichette per eliminarle tutte)", "DownloadClientPriorityHelpText": "Dai priorità a multipli Client di download. Round-Robin è usato per i client con la stessa priorità.", "DeleteSelectedDownloadClients": "Cancella i Client di Download", - "DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare l'indexer '{0}'?", "Album": "Album", "Artist": "Artista", "Label": "Etichetta", - "More": "Di più", + "More": "Altro", "Season": "Stagione", "Year": "Anno", - "UpdateAvailable": "É disponibile un nuovo aggiornamento", + "UpdateAvailableHealthCheckMessage": "Nuovo aggiornamento disponibile", "Author": "Autore", "ApplyChanges": "Applica Cambiamenti", - "ApiKeyValidationHealthCheckMessage": "Aggiorna la tua chiave API in modo che abbia una lunghezza di almeno {0} caratteri. Puoi farlo dalle impostazioni o dal file di configurazione", - "DeleteAppProfileMessageText": "Sicuro di voler cancellare il profilo di qualità {0}", - "RecentChanges": "Cambiamenti recenti", + "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": "Radarr cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.", - "ConnectionLostToBackend": "Radarr ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.", + "ConnectionLostReconnect": "{appName} cercherà di connettersi automaticamente, oppure clicca su ricarica qui sotto.", + "ConnectionLostToBackend": "{appName} ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.", "minutes": "Minuti", "AddConnection": "Aggiungi Connessione", "NotificationStatusAllClientHealthCheckMessage": "Tutte le applicazioni non sono disponibili a causa di errori", - "NotificationStatusSingleClientHealthCheckMessage": "Applicazioni non disponibili a causa di errori: {0}", - "AuthForm": "Moduli (Pagina di Accesso)", + "NotificationStatusSingleClientHealthCheckMessage": "Applicazioni 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?", - "AuthBasic": "Base (Popup del Browser)" + "AuthBasic": "Base (Popup del Browser)", + "EditSelectedDownloadClients": "Modifica i Client di Download Selezionati", + "AppUpdated": "{appName} Aggiornato", + "AddCustomFilter": "Aggiungi Filtro Personalizzato", + "AddConnectionImplementation": "Aggiungi Connessione - {implementationName}", + "AddDownloadClientImplementation": "Aggiungi un Client di Download - {implementationName}", + "AddIndexerImplementation": "Aggiungi indicizzatore - {implementationName}", + "AuthenticationRequiredUsernameHelpTextWarning": "Inserisci username", + "AuthenticationMethod": "Metodo di Autenticazione", + "AuthenticationMethodHelpTextWarning": "Selezione un metodo di autenticazione valido", + "AuthenticationRequiredPasswordHelpTextWarning": "Inserisci la nuova password", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Conferma la nuova password", + "Clone": "Copia", + "DefaultNameCopiedProfile": "{name} - Copia", + "AppUpdatedVersion": "{appName} è stato aggiornato alla versione `{version}`, per vedere le modifiche devi ricaricare {appName}", + "AddApplicationImplementation": "Aggiungi Connessione - {implementationName}", + "AddIndexerProxyImplementation": "Aggiungi indicizzatore - {implementationName}", + "EditApplicationImplementation": "Aggiungi Condizione - {implementationName}", + "CountApplicationsSelected": "{count} Collezione(i) Selezionate", + "EditConnectionImplementation": "Modifica Connessione - {implementationName}", + "EditDownloadClientImplementation": "Modifica Client di Download - {implementationName}", + "EditIndexerImplementation": "Modifica Indicizzatore - {implementationName}", + "EditIndexerProxyImplementation": "Aggiungi indicizzatore - {implementationName}", + "AdvancedSettingsShownClickToHide": "Impostazioni avanzate mostrate, clicca per nasconderle", + "AdvancedSettingsHiddenClickToShow": "Impostazioni avanzate nascoste, clicca per mostrarle", + "AddApplication": "Aggiungi Applicazione", + "AddCategory": "Aggiungi Categoria", + "ActiveApps": "App Attive", + "ActiveIndexers": "Indicizzatori Attivi", + "IndexerBeyondHDSettingsSearchTypes": "Tipi di Ricerca", + "Directory": "Cartella", + "CustomFilter": "Filtro Personalizzato", + "IndexerHDBitsSettingsCodecs": "Codec", + "IndexerHDBitsSettingsMediums": "medio", + "GrabRelease": "Preleva Release", + "ProxyValidationBadRequest": "Il test del proxy è fallito. Codice Stato: {statusCode}", + "Discord": "Discord", + "Donate": "Dona", + "Destination": "Destinazione", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientFreeboxSettingsAppId": "ID App", + "DownloadClientFreeboxSettingsAppToken": "Token App", + "DownloadClientPneumaticSettingsNzbFolder": "Cartella Nzb", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Questa cartella dovrà essere raggiungibile da XBMC", + "DownloadClientRTorrentSettingsUrlPath": "Percorso Url", + "Default": "Predefinito", + "DownloadClientPneumaticSettingsStrmFolder": "Cartella Strm", + "IndexerDisabled": "Indexer Disattivato", + "GoToApplication": "Vai all'applicazione", + "AreYouSureYouWantToDeleteIndexer": "Sei sicuro di voler eliminare '{name}' da {appName}?", + "IndexerStatus": "Stato Indicizzatore", + "XmlRpcPath": "Percorso XML RPC", + "EditCategory": "Modifica Categoria", + "IndexerSettingsAdditionalParameters": "Parametri Addizionali", + "IndexerSettingsApiPath": "Percorso API", + "IndexerSettingsVipExpiration": "Scadenza VIP", + "DefaultCategory": "Categoria Predefinita", + "DownloadClientFloodSettingsAdditionalTags": "Tag addizionali", + "IndexerHDBitsSettingsMediumsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "IndexerHDBitsSettingsOrigins": "Origini", + "IndexerHDBitsSettingsOriginsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "IndexerSettingsCookie": "Cookie", + "DeleteSelectedApplications": "Elimina Applicazioni Selezionate", + "IndexerHDBitsSettingsCodecsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "IndexerSettingsApiUser": "Utente API", + "PrioritySettings": "Priorità: {priority}", + "CountDownloadClientsSelected": "{count} client di download selezionato/i", + "NotificationsTelegramSettingsIncludeAppName": "Includi {appName} nel Titolo", + "Menu": "Menu", + "NoIndexersFound": "Nessun indicizzatore trovato", + "PasswordConfirmation": "Conferma Password", + "NoHistoryFound": "Nessun storico trovato", + "DeleteSelectedIndexersMessageText": "Sei sicuro di voler eliminare {count} applicazione(i) selezionata(e)?", + "UsenetBlackholeNzbFolder": "Cartella Nzb", + "VipExpiration": "Scadenza VIP", + "OverrideAndAddToDownloadClient": "Sovrascrivi e aggiungi alla coda di download", + "BasicSearch": "Ricerca basica", + "CountIndexersAvailable": "{count} indicizzatore/i disponibili", + "EditSelectedIndexers": "Modifica Indicizzatori Selezionati", + "FoundCountReleases": "Trovate {itemCount} release", + "ManageApplications": "Gestisci Applicazioni", + "ManageDownloadClients": "Gestisci Clients di Download", + "HistoryDetails": "Dettagli Storico", + "NotificationsEmailSettingsUseEncryption": "Usa Crittografia", + "SearchAllIndexers": "Cerca tutti gli indicizzatori", + "SearchCountIndexers": "Cerca {count} indicizzatore/i", + "SearchQueries": "Cerca Richieste", + "SeedRatio": "Rapporto Seed", + "TorznabUrl": "Url Torznab", + "TorrentBlackholeTorrentFolder": "Cartella Torrent", + "UseSsl": "Usa SSL", + "days": "giorni", + "IndexerCategories": "Categorie degli Indicizzatori", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "API Key Sito", + "LabelIsRequired": "Etichetta richiesta", + "NoIndexerHistory": "Nessun storico trovato per questo indicizzatore", + "RssFeed": "Feed RSS", + "AverageResponseTimesMs": "Tempo di Risposta Medio dell'Indicizzatore (ms)", + "DeleteSelectedIndexer": "Elimina Indicizzatore Selezionato", + "DisabledUntil": "Disattiva fino", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Aggiungi un prefisso all'url del json di deluge, vedi {url}", + "Implementation": "Implementazione", + "ManageClients": "Gestisci Clients", + "NewznabUrl": "Url Newznab", + "NoApplicationsFound": "Nessuna applicazione trovata", + "IndexerSettingsBaseUrl": "Url Base", + "IndexerId": "ID Indicizzatore", + "NoDownloadClientsFound": "Nessun client di download trovato", + "BlackholeFolderHelpText": "Cartella nella quale {appName} salverà i file di tipo {extension}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Questa opzione richiede almeno la versione 16.0 di NzbGet", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Scarica in ordine sequenziale (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una connessione sicura. Vedi Opzioni -> Web UI -> 'Usa HTTPS invece di HTTP' in qBittorrent.", + "DownloadClientRTorrentSettingsAddStopped": "Aggiungi Fermato", + "DownloadClientSettingsInitialState": "Stato Iniziale", + "DownloadClientSettingsInitialStateHelpText": "Stato iniziale per i torrent aggiunti a {clientName}", + "DownloadClientSettingsAddPaused": "Aggiungi In Pausa", + "DownloadClientSettingsUseSslHelpText": "Usa connessione sicura quando connetti a {clientName}", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent", + "IndexerSettingsApiPathHelpText": "Percorso API, solitamente {url}", + "IndexerSettingsBaseUrlHelpText": "Seleziona quale url base {appName} userà per le richieste al sito", + "NoIndexerCategories": "Nessuna categoria trovata per questo indicizzatore", + "SecretToken": "Secret Token", + "SeedRatioHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", + "TotalQueries": "Totale Richieste", + "IndexerHistoryLoadError": "Errore caricando lo storico dell'indicizzatore", + "DeleteSelectedIndexers": "Elimina Indicizzatori Selezionati", + "InvalidUILanguage": "L'interfaccia è impostata in una lingua non valida, correggi e salva le tue impostazioni", + "IndexerSettingsSeedRatio": "Rapporto Seed", + "IndexerSettingsRssKey": "Chiave RSS", + "RssQueries": "Richieste RSS", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordine Sequenziale", + "External": "Esterno", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Parametri Newznab addizionali", + "SelectDownloadClientModalTitle": "{modalTitle} - Seleziona Client di Download", + "DownloadClientSettingsDestinationHelpText": "Specifica manualmente la destinazione dei download, lascia vuoti per usare la predefinita", + "IndexerDownloadClientHealthCheckMessage": "Indicizzatori con client di download non validi: {indexerNames}.", + "SeedTimeHelpText": "Il rapporto che un torrent dovrebbe raggiungere prima di essere fermato, vuoto è il predefinito dell'app", + "IndexerPassThePopcornSettingsApiKeyHelpText": "API Key Sito", + "IndexerNzbIndexSettingsApiKeyHelpText": "API Key Sito", + "IndexerNewznabSettingsApiKeyHelpText": "API Key Sito", + "Fixed": "Fissato", + "Any": "Qualunque", + "BuiltIn": "Incluso", + "Script": "Script", + "InfoUrl": "URL Info", + "PublishedDate": "Data Pubblicazione", + "Redirected": "Reindirizzamento", + "AllSearchResultsHiddenByFilter": "Tutti i risultati sono nascosti dal filtro", + "PackageVersionInfo": "{packageVersion} di {packageAuthor}", + "DockerUpdater": "Aggiorna il container di docker per ricevere l'aggiornamento", + "Download": "Scarica", + "ErrorRestoringBackup": "Errore durante il ripristino del backup", + "ExternalUpdater": "{appName} è configurato per utilizzare un meccanismo di aggiornamento esterno", + "LogFilesLocation": "File di Log localizzati in: {location}", + "NoEventsFound": "Nessun evento trovato", + "RestartReloadNote": "Nota: {appName} si riavvierà automaticamente e ricaricherà l'interfaccia durante il processo di ripristino.", + "WouldYouLikeToRestoreBackup": "Vuoi ripristinare il backup '{name}'?", + "UpdateAppDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,", + "AptUpdater": "Usa apt per installare l'aggiornamento", + "InstallLatest": "Installa il più recente", + "CurrentlyInstalled": "Attualmente Installato", + "PreviouslyInstalled": "Precedentemente Installato", + "Mixed": "Fissato" } diff --git a/src/NzbDrone.Core/Localization/Core/ja.json b/src/NzbDrone.Core/Localization/Core/ja.json index 448d15e90..f75d051df 100644 --- a/src/NzbDrone.Core/Localization/Core/ja.json +++ b/src/NzbDrone.Core/Localization/Core/ja.json @@ -29,7 +29,7 @@ "OnHealthIssueHelpText": "健康問題について", "Priority": "優先", "Restart": "再起動", - "RSSIsNotSupportedWithThisIndexer": "RSSはこのインデクサーではサポートされていません", + "RssIsNotSupportedWithThisIndexer": "RSSはこのインデクサーではサポートされていません", "ShowSearch": "検索を表示", "SSLPort": "SSLポート", "StartupDirectory": "スタートアップディレクトリ", @@ -41,7 +41,7 @@ "UISettings": "UI設定", "UnableToAddANewApplicationPleaseTryAgain": "新しい通知を追加できません。もう一度やり直してください。", "UnableToLoadNotifications": "通知を読み込めません", - "UpdateCheckStartupNotWritableMessage": "スタートアップフォルダ「{0}」はユーザー「{1}」によって書き込み可能ではないため、更新をインストールできません。", + "UpdateStartupNotWritableHealthCheckMessage": "スタートアップフォルダ「{startupFolder}」はユーザー「{userName}」によって書き込み可能ではないため、更新をインストールできません。", "UpdateMechanismHelpText": "{appName}の組み込みアップデーターまたはスクリプトを使用する", "Wiki": "ウィキ", "Yesterday": "昨日", @@ -51,7 +51,7 @@ "DeleteIndexerProxyMessageText": "タグ「{0}」を削除してもよろしいですか?", "Edit": "編集", "DownloadClientSettings": "クライアント設定のダウンロード", - "DownloadClientStatusCheckAllClientMessage": "障害のため、すべてのダウンロードクライアントを利用できません", + "DownloadClientStatusAllClientHealthCheckMessage": "障害のため、すべてのダウンロードクライアントを利用できません", "EnableAutomaticSearch": "自動検索を有効にする", "EnableAutomaticSearchHelpText": "UIまたはRadarによって自動検索が実行されるときに使用されます", "Enabled": "有効", @@ -60,9 +60,9 @@ "Filename": "ファイル名", "Files": "ファイル", "Filter": "フィルタ", - "IndexerLongTermStatusCheckAllClientMessage": "6時間以上の障害のため、すべてのインデクサーが使用できなくなります", - "IndexerProxyStatusCheckAllClientMessage": "障害のため、すべてのインデクサーを使用できません", - "IndexerProxyStatusCheckSingleClientMessage": "失敗のため利用できないリスト:{0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6時間以上の障害のため、すべてのインデクサーが使用できなくなります", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "障害のため、すべてのインデクサーを使用できません", + "IndexerProxyStatusUnavailableHealthCheckMessage": "失敗のため利用できないリスト:{indexerProxyNames}", "Indexers": "インデクサー", "LastWriteTime": "最終書き込み時間", "Level": "レベル", @@ -100,7 +100,7 @@ "TagCannotBeDeletedWhileInUse": "使用中は削除できません", "TagIsNotUsedAndCanBeDeleted": "タグは使用されておらず、削除できます", "TestAll": "すべてテスト", - "UnableToLoadDownloadClients": "ダウンロードクライアントを読み込めません", + "DownloadClientsLoadError": "ダウンロードクライアントを読み込めません", "UnableToLoadGeneralSettings": "一般設定を読み込めません", "UpdateAutomaticallyHelpText": "アップデートを自動的にダウンロードしてインストールします。 System:Updatesから引き続きインストールできます。", "Updates": "更新", @@ -109,7 +109,7 @@ "AddDownloadClient": "ダウンロードクライアントの追加", "BackupFolderHelpText": "相対パスは{appName}のAppDataディレクトリの下にあります", "DownloadClient": "クライアントのダウンロード", - "DownloadClientStatusCheckSingleClientMessage": "失敗のためにダウンロードできないクライアント:{0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "失敗のためにダウンロードできないクライアント:{downloadClientNames}", "Restore": "戻す", "EnableRss": "RSSを有効にする", "EnableInteractiveSearch": "インタラクティブ検索を有効にする", @@ -133,7 +133,7 @@ "Connections": "接続", "ConnectSettings": "接続設定", "Options": "オプション", - "IndexerStatusCheckAllClientMessage": "障害のため、すべてのインデクサーを使用できません", + "IndexerStatusAllUnavailableHealthCheckMessage": "障害のため、すべてのインデクサーを使用できません", "Reddit": "Reddit", "Today": "今日", "Tomorrow": "明日", @@ -141,7 +141,7 @@ "UI": "UI", "CouldNotConnectSignalR": "SignalRに接続できませんでした。UIは更新されません", "Custom": "カスタム", - "DBMigration": "DB移行", + "DatabaseMigration": "DB移行", "Delete": "削除", "DeleteApplicationMessageText": "通知「{0}」を削除してもよろしいですか?", "DeleteBackup": "バックアップを削除する", @@ -171,9 +171,9 @@ "Protocol": "プロトコル", "Proxy": "プロキシ", "ProxyBypassFilterHelpText": "'、'を区切り文字として使用し、 '*。'を使用します。サブドメインのワイルドカードとして", - "ProxyCheckBadRequestMessage": "プロキシのテストに失敗しました。 StatusCode:{0}", - "ProxyCheckFailedToTestMessage": "プロキシのテストに失敗しました:{0}", - "ProxyCheckResolveIpMessage": "構成済みプロキシホスト{0}のIPアドレスの解決に失敗しました", + "ProxyBadRequestHealthCheckMessage": "プロキシのテストに失敗しました。 StatusCode:{statusCode}", + "ProxyFailedToTestHealthCheckMessage": "プロキシのテストに失敗しました:{url}", + "ProxyResolveIpHealthCheckMessage": "構成済みプロキシホスト{proxyHostName}のIPアドレスの解決に失敗しました", "ProxyPasswordHelpText": "ユーザー名とパスワードが必要な場合にのみ入力する必要があります。それ以外の場合は空白のままにします。", "ProxyType": "プロキシタイプ", "RefreshMovie": "映画を更新する", @@ -182,7 +182,7 @@ "RestartNow": "今すぐ再起動", "Result": "結果", "Retention": "保持", - "RSS": "RSS", + "Rss": "RSS", "Save": "保存する", "SaveChanges": "変更内容を保存", "Scheduled": "予定", @@ -195,7 +195,7 @@ "Size": "サイズ", "Sort": "ソート", "SSLCertPath": "SSL証明書パス", - "SystemTimeCheckMessage": "システム時刻が1日以上ずれています。スケジュールされたタスクは、時間が修正されるまで正しく実行されない場合があります", + "SystemTimeHealthCheckMessage": "システム時刻が1日以上ずれています。スケジュールされたタスクは、時間が修正されるまで正しく実行されない場合があります", "Tags": "タグ", "TestAllClients": "すべてのクライアントをテストする", "Torrents": "トレント", @@ -206,7 +206,7 @@ "UnableToAddANewDownloadClientPleaseTryAgain": "新しいダウンロードクライアントを追加できません。もう一度やり直してください。", "UnableToAddANewIndexerPleaseTryAgain": "新しいインデクサーを追加できません。もう一度やり直してください。", "UnableToAddANewNotificationPleaseTryAgain": "新しい通知を追加できません。もう一度やり直してください。", - "UnableToLoadBackups": "バックアップを読み込めません", + "BackupsLoadError": "バックアップを読み込めません", "UnableToLoadHistory": "履歴を読み込めません", "UnableToLoadTags": "タグを読み込めません", "UnableToLoadUISettings": "UI設定を読み込めません", @@ -215,8 +215,8 @@ "BackupRetentionHelpText": "保存期間より古い自動バックアップは自動的にクリーンアップされます", "Backups": "バックアップ", "CertificateValidation": "証明書の検証", - "UpdateCheckStartupTranslocationMessage": "スタートアップフォルダー '{0}'がAppTranslocationフォルダーにあるため、更新をインストールできません。", - "UpdateCheckUINotWritableMessage": "UIフォルダー「{0}」はユーザー「{1}」によって書き込み可能ではないため、更新をインストールできません。", + "UpdateStartupTranslocationHealthCheckMessage": "スタートアップフォルダー '{startupFolder}'がAppTranslocationフォルダーにあるため、更新をインストールできません。", + "UpdateUiNotWritableHealthCheckMessage": "UIフォルダー「{uiFolder}」はユーザー「{userName}」によって書き込み可能ではないため、更新をインストールできません。", "URLBase": "URLベース", "UrlBaseHelpText": "リバースプロキシサポートの場合、デフォルトは空です", "UseProxy": "プロキシを使う", @@ -248,7 +248,7 @@ "GeneralSettings": "一般設定", "Grabbed": "掴んだ", "Health": "健康", - "HealthNoIssues": "構成に問題はありません", + "NoIssuesWithYourConfiguration": "構成に問題はありません", "HideAdvanced": "高度な非表示", "EnableInteractiveSearchHelpText": "インタラクティブ検索を使用する場合に使用されます", "Error": "エラー", @@ -298,8 +298,8 @@ "Host": "ホスト", "Hostname": "ホスト名", "IllRestartLater": "後で再起動します", - "IndexerLongTermStatusCheckSingleClientMessage": "6時間以上の障害のため、インデクサーを使用できません:{0}", - "IndexerStatusCheckSingleClientMessage": "失敗のためインデクサーを利用できません:{0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6時間以上の障害のため、インデクサーを使用できません:{indexerNames}", + "IndexerStatusUnavailableHealthCheckMessage": "失敗のためインデクサーを利用できません:{indexerNames}", "Info": "情報", "NoLogFiles": "ログファイルがありません", "NoTagsHaveBeenAddedYet": "タグはまだ追加されていません", @@ -329,7 +329,7 @@ "Queued": "キューに入れられました", "Remove": "削除する", "Replace": "交換", - "TheLatestVersionIsAlreadyInstalled": "{appName}の最新バージョンはすでにインストールされています", + "OnLatestVersion": "{appName}の最新バージョンはすでにインストールされています", "Track": "痕跡", "DeleteSelectedDownloadClients": "ダウンロードクライアントを削除する", "Genre": "ジャンル", @@ -346,11 +346,34 @@ "RecentChanges": "最近の変化", "WhatsNew": "新着情報?", "minutes": "議事録", - "NotificationStatusSingleClientHealthCheckMessage": "失敗のため利用できないリスト:{0}", + "NotificationStatusSingleClientHealthCheckMessage": "失敗のため利用できないリスト:{notificationNames}", "NotificationStatusAllClientHealthCheckMessage": "障害のため、すべてのリストを利用できません", "AuthBasic": "基本(ブラウザポップアップ)", "AuthForm": "フォーム(ログインページ)", "DisabledForLocalAddresses": "ローカルアドレスでは無効", "None": "なし", - "ResetAPIKeyMessageText": "APIキーをリセットしてもよろしいですか?" + "ResetAPIKeyMessageText": "APIキーをリセットしてもよろしいですか?", + "RestartProwlarr": "{appName}を再起動します", + "IndexerHDBitsSettingsMediums": "中", + "CustomFilter": "カスタムフィルター", + "ProxyValidationBadRequest": "プロキシのテストに失敗しました。 StatusCode:{statusCode}", + "GrabRelease": "グラブリリース", + "Script": "脚本", + "BuiltIn": "ビルトイン", + "PublishedDate": "公開日", + "AllSearchResultsHiddenByFilter": "すべての結果は、適用されたフィルターによって非表示になります", + "DockerUpdater": "Dockerコンテナを更新して、更新を受信します", + "Download": "ダウンロード", + "ErrorRestoringBackup": "バックアップの復元中にエラーが発生しました", + "ExternalUpdater": "{appName}は、外部更新メカニズムを使用するように構成されています", + "NoEventsFound": "イベントが見つかりません", + "RestartReloadNote": "注:{appName}は、復元プロセス中にUIを自動的に再起動して再読み込みします。", + "UpdateAppDirectlyLoadError": "{appName}を直接更新できません。", + "AptUpdater": "aptを使用してアップデートをインストールします", + "InstallLatest": "最新のインストール", + "Clone": "閉じる", + "Mixed": "修繕", + "CurrentlyInstalled": "現在インストール中", + "Stats": "状態", + "Season": "理由" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 4d5589aa5..5a30e0f08 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -26,7 +26,7 @@ "Connections": "연결", "CouldNotConnectSignalR": "SignalR에 연결할 수 없습니다. UI가 업데이트되지 않습니다.", "Custom": "사용자 지정", - "DBMigration": "DB 마이그레이션", + "DatabaseMigration": "DB 마이그레이션", "DeleteBackupMessageText": "백업 '{0}'을(를) 삭제하시겠습니까?", "BackupNow": "지금 백업", "Authentication": "인증", @@ -52,7 +52,7 @@ "Donations": "기부", "DownloadClient": "클라이언트 다운로드", "DownloadClientSettings": "클라이언트 설정 다운로드", - "DownloadClientStatusCheckAllClientMessage": "실패로 인해 모든 다운로드 클라이언트를 사용할 수 없습니다.", + "DownloadClientStatusAllClientHealthCheckMessage": "실패로 인해 모든 다운로드 클라이언트를 사용할 수 없습니다.", "Add": "추가", "Apply": "적용", "AppDataLocationHealthCheckMessage": "업데이트 시 AppData 삭제를 방지하기 위해 업데이트 할 수 없습니다.", @@ -66,7 +66,7 @@ "DeleteDownloadClientMessageText": "다운로드 클라이언트 '{0}'을(를) 삭제하시겠습니까?", "DeleteNotification": "알림 삭제", "DeleteNotificationMessageText": "알림 '{0}'을(를) 삭제하시겠습니까?", - "DownloadClientStatusCheckSingleClientMessage": "실패로 인해 다운 불러올 수 없는 클라이언트 : {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "실패로 인해 다운 불러올 수 없는 클라이언트 : {downloadClientNames}", "MoreInfo": "더 많은 정보", "Edit": "편집하다", "Indexer": "인덱서", @@ -85,11 +85,11 @@ "UnableToAddANewAppProfilePleaseTryAgain": "새 품질 프로필을 추가 할 수 없습니다. 다시 시도하십시오.", "UnableToAddANewDownloadClientPleaseTryAgain": "새 다운로드 클라이언트를 추가 할 수 없습니다. 다시 시도하십시오.", "UnableToAddANewIndexerPleaseTryAgain": "새 인덱서를 추가 할 수 없습니다. 다시 시도하십시오.", - "UnableToLoadBackups": "백업을로드 할 수 없습니다.", + "BackupsLoadError": "백업을로드 할 수 없습니다.", "UpdateAutomaticallyHelpText": "업데이트를 자동으로 다운로드하고 설치합니다. 시스템 : 업데이트에서 계속 설치할 수 있습니다.", "RemoveFilter": "필터 제거", "Size": "크기", - "AllIndexersHiddenDueToFilter": "적용된 필터로 인해 모든 영화가 숨겨집니다.", + "AllIndexersHiddenDueToFilter": "적용된 필터로 인해 모든 인덱서가 숨겨집니다.", "Reset": "초기화", "Enable": "활성화", "Enabled": "활성화", @@ -115,10 +115,10 @@ "ErrorLoadingContents": "콘텐츠로드 오류", "Grabs": "붙잡다", "Torrent": "급류", - "Torrents": "급류", + "Torrents": "토렌트", "Type": "유형", "DeleteApplicationMessageText": "알림 '{0}'을(를) 삭제하시겠습니까?", - "AuthenticationMethodHelpText": "{appName}에 액세스하려면 사용자 이름 및 암호 필요", + "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다", "BackupFolderHelpText": "상대 경로는 {appName}의 AppData 디렉토리에 있습니다.", "Branch": "분기", "BranchUpdate": "{appName} 업데이트에 사용할 분기", @@ -142,7 +142,7 @@ "Restore": "복원", "Result": "결과", "Retention": "보유", - "RSSIsNotSupportedWithThisIndexer": "이 인덱서에서는 RSS가 지원되지 않습니다.", + "RssIsNotSupportedWithThisIndexer": "이 인덱서에서는 RSS가 지원되지 않습니다.", "Scheduled": "예정", "ScriptPath": "스크립트 경로", "Search": "검색", @@ -160,7 +160,7 @@ "Title": "표제", "Today": "오늘", "UILanguageHelpTextWarning": "브라우저 새로 고침 필요", - "UnableToLoadDownloadClients": "다운로드 클라이언트를로드 할 수 없습니다.", + "DownloadClientsLoadError": "다운로드 클라이언트를로드 할 수 없습니다.", "UnableToLoadHistory": "기록을로드 할 수 없습니다.", "UnableToLoadTags": "태그를로드 할 수 없습니다.", "UnableToLoadUISettings": "UI 설정을로드 할 수 없습니다.", @@ -186,7 +186,7 @@ "UnableToAddANewNotificationPleaseTryAgain": "새 알림을 추가 할 수 없습니다. 다시 시도하십시오.", "UnableToLoadGeneralSettings": "일반 설정을로드 할 수 없습니다.", "UnableToLoadNotifications": "알림을로드 할 수 없습니다.", - "UpdateMechanismHelpText": "{appName}의 내장 업데이트 프로그램 또는 스크립트 사용", + "UpdateMechanismHelpText": "{appName}의 내장 업데이트 도구 또는 스크립트 사용", "UpdateScriptPathHelpText": "추출 된 업데이트 패키지를 사용하고 나머지 업데이트 프로세스를 처리하는 사용자 지정 스크립트에 대한 경로", "URLBase": "URL베이스", "Usenet": "유즈넷", @@ -236,7 +236,7 @@ "FocusSearchBox": "포커스 검색 창", "GeneralSettingsSummary": "포트, SSL, 사용자 이름 / 암호, 프록시, 분석 및 업데이트", "HideAdvanced": "고급 숨기기", - "IndexerStatusCheckAllClientMessage": "오류로 인해 모든 인덱서를 사용할 수 없습니다.", + "IndexerStatusAllUnavailableHealthCheckMessage": "오류로 인해 모든 인덱서를 사용할 수 없습니다.", "InteractiveSearch": "대화형 검색", "LastWriteTime": "마지막 쓰기 시간", "Link": "연결", @@ -248,17 +248,17 @@ "PendingChangesMessage": "저장하지 않은 변경 사항이 있습니다.이 페이지에서 나가시겠습니까?", "PendingChangesStayReview": "변경 사항 유지 및 검토", "Presets": "사전 설정", - "ProxyCheckResolveIpMessage": "구성된 프록시 호스트 {0}의 IP 주소를 확인하지 못했습니다.", + "ProxyResolveIpHealthCheckMessage": "구성된 프록시 호스트 {proxyHostName}의 IP 주소를 확인하지 못했습니다.", "Reddit": "레딧", "TagsSettingsSummary": "모든 태그와 사용 방법을 확인하십시오. 사용하지 않는 태그는 제거 할 수 있습니다.", "Yesterday": "어제", "ApplicationStatusCheckAllClientMessage": "실패로 인해 모든 목록을 사용할 수 없습니다.", "ApplicationStatusCheckSingleClientMessage": "실패로 인해 사용할 수없는 목록 : {0}", "ReleaseBranchCheckOfficialBranchMessage": "{0} 분기는 유효한 Whisparr 출시 분기가 아닙니다. 업데이트를받을 수 없습니다.", - "ProxyCheckBadRequestMessage": "프록시를 테스트하지 못했습니다. StatusCode : {0}", - "ProxyCheckFailedToTestMessage": "프록시 테스트 실패 : {0}", + "ProxyBadRequestHealthCheckMessage": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "프록시 테스트 실패 : {url}", "ReleaseStatus": "출시 상태", - "RSS": "RSS", + "Rss": "RSS", "Sort": "종류", "UnsavedChanges": "저장되지 않은 변경 사항", "UnselectAll": "모두 선택 해제", @@ -271,15 +271,15 @@ "RefreshMovie": "영화 새로 고침", "SuggestTranslationChange": "번역 변경 제안", "Level": "수평", - "UpdateCheckStartupTranslocationMessage": "시작 폴더 '{0}'이 (가) App Translocation 폴더에 있으므로 업데이트를 설치할 수 없습니다.", + "UpdateStartupTranslocationHealthCheckMessage": "시작 폴더 '{startupFolder}'이 (가) App Translocation 폴더에 있으므로 업데이트를 설치할 수 없습니다.", "UrlBaseHelpText": "역방향 프록시 지원의 경우 기본값은 비어 있습니다.", "MovieIndexScrollBottom": "영화 색인 : 아래로 스크롤", - "View": "전망", + "View": "화면", "Wiki": "위키", "EditIndexer": "인덱서 편집", "Filter": "필터", "Health": "건강", - "HealthNoIssues": "구성에 문제 없음", + "NoIssuesWithYourConfiguration": "구성에 문제 없음", "Info": "정보", "KeyboardShortcuts": "키보드 단축키", "MovieIndexScrollTop": "영화 색인 : 상단 스크롤", @@ -288,7 +288,7 @@ "Priority": "우선 순위", "SetTags": "태그 설정", "System": "체계", - "UpdateCheckUINotWritableMessage": "사용자 '{1}'이 (가) UI 폴더 '{0}'에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.", + "UpdateUiNotWritableHealthCheckMessage": "사용자 '{userName}'이 (가) UI 폴더 '{uiFolder}'에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.", "Warn": "경고", "HomePage": "홈 페이지", "Peers": "동료", @@ -299,36 +299,36 @@ "Failed": "실패한", "FeatureRequests": "기능 요청", "IndexerFlags": "인덱서 플래그", - "IndexerLongTermStatusCheckAllClientMessage": "6 시간 이상 오류로 인해 모든 인덱서를 사용할 수 없습니다.", - "IndexerLongTermStatusCheckSingleClientMessage": "6 시간 이상 오류로 인해 인덱서를 사용할 수 없음 : {0}", - "IndexerProxyStatusCheckAllClientMessage": "오류로 인해 모든 인덱서를 사용할 수 없습니다.", - "IndexerProxyStatusCheckSingleClientMessage": "오류로 인해 인덱서를 사용할 수 없음 : {0}", - "IndexerStatusCheckSingleClientMessage": "오류로 인해 인덱서를 사용할 수 없음 : {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 시간 이상 오류로 인해 모든 인덱서를 사용할 수 없습니다.", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 시간 이상 오류로 인해 인덱서를 사용할 수 없음 : {indexerNames}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "오류로 인해 모든 인덱서를 사용할 수 없습니다.", + "IndexerProxyStatusUnavailableHealthCheckMessage": "오류로 인해 인덱서를 사용할 수 없음 : {indexerProxyNames}", + "IndexerStatusUnavailableHealthCheckMessage": "오류로 인해 인덱서를 사용할 수 없음 : {indexerNames}", "NetCore": ".NET Core", "Save": "저장", "SaveSettings": "설정 저장", "Seeders": "시더", "SelectAll": "모두 선택", "ShowAdvanced": "고급보기", - "SystemTimeCheckMessage": "시스템 시간이 1 일 이상 꺼져 있습니다. 예약 된 작업은 시간이 수정 될 때까지 올바르게 실행되지 않을 수 있습니다.", + "SystemTimeHealthCheckMessage": "시스템 시간이 1 일 이상 꺼져 있습니다. 예약 된 작업은 시간이 수정 될 때까지 올바르게 실행되지 않을 수 있습니다.", "TableOptions": "테이블 옵션", "TableOptionsColumnsMessage": "표시되는 열과 표시되는 순서 선택", "TagsHelpText": "일치하는 태그가 하나 이상있는 영화에 적용됩니다.", "Test": "테스트", "Time": "시간", "UnableToLoadIndexers": "인덱서를로드 할 수 없습니다.", - "UpdateCheckStartupNotWritableMessage": "'{1}'사용자가 '{0}'시작 폴더에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.", + "UpdateStartupNotWritableHealthCheckMessage": "'{userName}'사용자가 '{startupFolder}'시작 폴더에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.", "Yes": "예", "GrabReleases": "그랩 릴리스", "NextExecution": "다음 실행", "ApplicationLongTermStatusCheckSingleClientMessage": "6 시간 이상 오류로 인해 인덱서를 사용할 수 없음 : {0}", - "ApplicationLongTermStatusCheckAllClientMessage": "6 시간 이상 오류로 인해 모든 인덱서를 사용할 수 없습니다.", + "ApplicationLongTermStatusCheckAllClientMessage": "6 시간 이상 오류로 인해 모든 어플리케이션을 사용할 수 없습니다.", "Ended": "종료", "LastDuration": "lastDuration", "LastExecution": "마지막 실행", "Queued": "대기 중", "Replace": "바꾸다", - "TheLatestVersionIsAlreadyInstalled": "최신 버전의 Whisparr가 이미 설치되어 있습니다.", + "OnLatestVersion": "최신 버전의 Whisparr가 이미 설치되어 있습니다.", "Remove": "없애다", "Genre": "장르", "ApplyTagsHelpTextAdd": "추가 : 기존 태그 목록에 태그를 추가합니다.", @@ -346,11 +346,165 @@ "ConnectionLostToBackend": "Radarr는 백엔드와의 연결이 끊어졌으며 기능을 복원하려면 다시 로딩해야 합니다.", "DeleteAppProfileMessageText": "품질 프로필 {0}을 (를) 삭제 하시겠습니까?", "NotificationStatusAllClientHealthCheckMessage": "실패로 인해 모든 목록을 사용할 수 없습니다.", - "NotificationStatusSingleClientHealthCheckMessage": "실패로 인해 사용할 수없는 목록 : {0}", + "NotificationStatusSingleClientHealthCheckMessage": "실패로 인해 사용할 수없는 목록 : {notificationNames}", "AuthBasic": "기본 (브라우저 팝업)", "AuthForm": "양식 (로그인 페이지)", "DisabledForLocalAddresses": "로컬 주소에 대해 비활성화됨", "None": "없음", "ResetAPIKeyMessageText": "API 키를 재설정하시겠습니까?", - "StopSelecting": "선택 취소" + "StopSelecting": "선택 취소", + "RestartProwlarr": "{appName} 다시 시작", + "IndexerHDBitsSettingsMediums": "매질", + "CustomFilter": "사용자 지정 필터", + "GrabRelease": "그랩 릴리스", + "ProxyValidationBadRequest": "프록시를 테스트하지 못했습니다. StatusCode : {statusCode}", + "BuiltIn": "내장", + "PublishedDate": "발행일", + "AllSearchResultsHiddenByFilter": "적용된 필터에 의해 모든 결과가 숨겨집니다.", + "DockerUpdater": "Docker 컨테이너를 업데이트하여 업데이트를 받으십시오.", + "Download": "다운로드", + "ErrorRestoringBackup": "백업 복원 오류", + "ExternalUpdater": "{appName}는 외부 업데이트 메커니즘을 사용하도록 구성됩니다.", + "RestartReloadNote": "참고 : {appName}는 복원 프로세스 중에 UI를 자동으로 다시 시작하고 다시로드합니다.", + "UpdateAppDirectlyLoadError": "{appName}를 직접 업데이트 할 수 없습니다.", + "AptUpdater": "apt를 사용하여 업데이트 설치", + "ActiveIndexers": "활성 인덱서", + "AddConnectionImplementation": "연결 추가 - {implementationName}", + "AddIndexerImplementation": "인덱서 추가 - {implementationName}", + "AddIndexerProxyImplementation": "인덱서 프록시 추가 - {implementationName}", + "Any": "모두", + "AppUpdatedVersion": "{appName}이 버전 `{version}`으로 업데이트되었습니다. 최신 변경 사항을 받으려면 {appName}을 다시 로드해야 합니다", + "AddRemoveOnly": "추가 및 제거만", + "AddToDownloadClient": "다운로드 클라이언트에 릴리스 추가", + "AddedToDownloadClient": "클라이언트에 릴리스 추가됨", + "AdvancedSettingsHiddenClickToShow": "고급 설정은 숨겨져 있으며, 표시하려면 클릭하세요", + "AdvancedSettingsShownClickToHide": "고급 설정 표시, 숨기려면 클릭", + "AddDownloadClientToProwlarr": "다운로드 클라이언트를 추가하면 {appName}이 수동 검색을 수행하는 동안 UI에서 직접 릴리스를 보낼 수 있습니다.", + "AddApplication": "애플리케이션 추가", + "AddCustomFilter": "커스텀 필터 추가", + "AddIndexerProxy": "인덱서 프록시 추가", + "AppUpdated": "{appName} 업데이트", + "Application": "어플리케이션", + "AppProfileInUse": "사용중인 앱 프로필", + "AppSettingsSummary": "{appName}이 PVR 프로그램과 상호 작용하는 방식을 구성하기 위한 애플리케이션 및 설정", + "AddNewIndexer": "새로운 인덱서 추가", + "AddSyncProfile": "동기화 프로필 추가", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "동기화 중 차단 목록에 있는 토렌트 해시 거부", + "AddApplicationImplementation": "애플리케이션 추가 - {implementationName}", + "AddCategory": "카테고리 추가", + "AddConnection": "연결 추가", + "ActiveApps": "활성 앱", + "AddDownloadClientImplementation": "다운로드 클라이언트 추가 - {implementationName}", + "Album": "앨범", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "해시에 의해 토렌트가 차단된 경우 일부 인덱서의 RSS/검색 중에 토렌트가 제대로 거부되지 않을 수 있습니다. 이 기능을 활성화하면 토렌트를 가져온 후 클라이언트로 전송하기 전에 토렌트를 거부할 수 있습니다.", + "ApiKeyValidationHealthCheckMessage": "API 키를 {length}자 이상으로 업데이트하세요. 설정 또는 구성 파일을 통해 이 작업을 수행할 수 있습니다.", + "AppProfileSelectHelpText": "앱 프로필은 애플리케이션 동기화에서 RSS, 자동 검색 및 대화형 검색 설정을 제어하는 데 사용됩니다", + "EditIndexerImplementation": "인덱서 추가 - {implementationName}", + "EditDownloadClientImplementation": "다운로드 클라이언트 추가 - {implementationName}", + "Clone": "닫기", + "EditApplicationImplementation": "애플리케이션 추가 - {implementationName}", + "Season": "이유", + "EditConnectionImplementation": "연결 추가 - {implementationName}", + "EditSyncProfile": "동기화 프로필 추가", + "CurrentlyInstalled": "현재 설치됨", + "Mixed": "결정된", + "Stats": "상태", + "Applications": "어플리케이션", + "EditIndexerProxyImplementation": "인덱서 프록시 추가 - {implementationName}", + "WouldYouLikeToRestoreBackup": "'{name}' 백업을 복원하시겠습니까?", + "XmlRpcPath": "XML RPC 경로", + "UpdateAvailableHealthCheckMessage": "새 업데이트 사용 가능: {version}", + "UsenetBlackholeNzbFolder": "Nzb 폴더", + "UseSsl": "SSL 사용", + "TorrentBlackholeTorrentFolder": "토렌트 폴더", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb 폴더", + "UserAgentProvidedByTheAppThatCalledTheAPI": "API를 호출한 앱에서 제공하는 사용자 에이전트", + "days": "일", + "minutes": "분", + "Author": "저작자", + "Categories": "카테고리", + "SeedRatio": "종자 비율", + "AuthenticationRequiredHelpText": "필수 인증을 요청하는 변경 사항. 위험을 이해하지 못한다면 변경하지 마세요.", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "미디어의 속성을 태그로 추가합니다. 힌트는 예시입니다.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "활성화하면 rTorrent에 정지된 상태에서 토런트와 마그넷이 추가됩니다. 마그넷 파일이 손상될 수 있습니다.", + "HealthMessagesInfoBox": "행 끝에 있는 위키 링크(책 아이콘)를 클릭하거나 [로그]({link})를 확인하면 이러한 상태 점검 메시지의 원인에 대한 상세 정보를 찾을 수 있습니다. 이러한 메시지를 해석하는 데 어려움이 있는 경우 아래 링크에서 지원팀에 문의할 수 있습니다.", + "DownloadClientSettingsDestinationHelpText": "다운로드 대상을 수동으로 지정하고 기본값을 사용하려면 비워두세요.", + "DownloadClientSettingsInitialStateHelpText": "{clientName}에 추가된 토런트의 초기 상태", + "IndexerSettingsAdditionalParameters": "매개 변수 추가", + "NoEventsFound": "이벤트가 없음", + "BlackholeFolderHelpText": "{appName}가 {extension} 파일을 저장할 폴더", + "DownloadClientSettingsUrlBaseHelpText": "{clientName} url에 {url}과 같은 접두사를 추가합니다.", + "DownloadClientQbittorrentSettingsContentLayout": "콘텐츠 레이아웃", + "Install": "설치", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "새 비밀번호 확인", + "FailedToFetchSettings": "설정을 가져오는데 실패함", + "Default": "기본값", + "Episode": "에피소드", + "AuthenticationMethod": "인증 방법", + "IndexerSettingsSeedRatio": "시드 비율", + "Destination": "대상", + "DownloadClientFreeboxSettingsAppToken": "앱 토큰", + "Logout": "로그아웃", + "DownloadClientFreeboxSettingsApiUrl": "API 주소", + "IndexerHDBitsSettingsCodecs": "코덱", + "WhatsNew": "새로운 소식?", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox 인터페이스에 액세스하는 데 사용되는 포트, 기본값은 '{port}'", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "이 폴더의 .strm 파일은 드론으로 가져옵니다.", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "qBittorrent에 추가된 토렌트의 초기 상태입니다. 강제 토렌트는 시드 제한을 따르지 않는다는 점에 유의하세요.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "다운로드를 넣을 선택 위치, 기본 rTorrent 위치를 사용하려면 비워두세요.", + "DownloadClientSettingsUseSslHelpText": "{clientName}에 연결할 때 보안 연결을 사용", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "{clientName} rpc URL에 접두사를 추가합니다(예: {url}, 기본값은 '{defaultUrl}')", + "InstallMajorVersionUpdateMessageLink": "상세 내용은 [{domain}]({url})을 확인하세요.", + "ApplicationUrlHelpText": "이 애플리케이션의 외부 URL - http(s)://, port 및 URL 기반 포함", + "Theme": "테마", + "ApplicationURL": "애플리케이션 URL", + "Directory": "디렉토리", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "다운로드를 넣을 공유 폴더(선택 사항). 기본 다운로드 스테이션 위치를 사용하려면 비워두세요.", + "DownloadClientFloodSettingsAdditionalTags": "추가 태그", + "DownloadClientFloodSettingsUrlBaseHelpText": "{url}와 같은 Flood API에 접두사를 추가합니다", + "DownloadClientFreeboxSettingsApiUrlHelpText": "API 버전을 사용하여 Freebox API 기본 URL을 정의하세요, 예를 들어 '{url}', 기본값은 '{defaultApiUrl}')", + "DownloadClientFreeboxSettingsAppId": "앱 ID", + "DownloadClientNzbgetSettingsAddPausedHelpText": "이 옵션을 사용하려면 최소한 NzbGet 버전 16.0이 필요합니다", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "이 폴더는 XBMC에서 접근할 수 있어야 합니다.", + "DownloadClientRTorrentSettingsAddStopped": "중지됨 추가", + "Category": "카테고리", + "DownloadClientTransmissionSettingsDirectoryHelpText": "다운로드를 넣을 위치 (선택 사항), 기본 전송 위치를 사용하려면 비워두세요", + "External": "외부", + "IndexerSettingsSeedRatioHelpText": "토렌드가 멈추기 전에 도달해야 하는 비율, 비어 있을 경우 다운로드 클라이언트의 기본값을 사용합니다. 비율은 최소 1.0이어야 하며 인덱서 규칙을 따라야 합니다", + "IndexerSettingsSeedTimeHelpText": "토렌드가 중지되기 전에 시드되어야 하는 시간, 비어 있을 경우 다운로드 클라이언트의 기본값을 사용합니다", + "InvalidUILanguage": "UI가 잘못된 언어로 설정되어 있습니다, 수정하고 설정을 저장하세요", + "AuthenticationRequired": "인증 필요", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "다른 애플리케이션의 알림을 구분하기 위해 메시지 제목 앞에 {appName}를 접두사로 사용 (선택 사항)", + "PackageVersionInfo": "{packageVersion} by {packageAuthor}", + "Duration": "기간", + "Script": "스크립트", + "SelectDownloadClientModalTitle": "{modalTitle} - 다운로드 클라이언트 선택", + "TheLogLevelDefault": "로그 수준의 기본값은 '정보'이며 [일반 설정](/settings/general)에서 변경할 수 있습니다", + "UpdaterLogFiles": "업데이트 도구 로그 파일", + "CountDownloadClientsSelected": "{count}개의 다운로드 클라이언트를 선택함", + "DefaultNameCopiedProfile": "{name} - 복사", + "DownloadClientDelugeSettingsUrlBaseHelpText": "deluge json url에 접두사를 추가합니다. {url}을(를) 참조하세요", + "FailedToFetchUpdates": "업데이트를 가져오는데 실패함", + "ApplyChanges": "변경 사항 적용", + "Started": "시작됨", + "Database": "데이터베이스", + "PasswordConfirmation": "비밀번호 확인", + "TorrentBlackholeSaveMagnetFiles": "마그넷 파일 저장", + "TorrentBlackholeSaveMagnetFilesExtension": "마그넷 파일 확장자 저장", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "마그넷 링크에 사용할 확장자, 기본값은 '.magnet'입니다", + "TorrentBlackholeSaveMagnetFilesHelpText": ".torrent 파일을 사용할 수 없는 경우 마그넷 링크를 저장합니다 (다운로드 클라이언트가 파일에 저장된 마그넷을 지원하는 경우에만 유용함)", + "AuthenticationMethodHelpTextWarning": "유효한 인증 방법을 선택해주세요", + "AuthenticationRequiredPasswordHelpTextWarning": "새로운 비밀번호를 입력하세요", + "AuthenticationRequiredUsernameHelpTextWarning": "새로운 사용자이름을 입력하세요", + "AuthenticationRequiredWarning": "인증 없이 원격 액세스를 방지하기 위해 {appName}은(는) 이제 인증을 활성화해야 합니다. 선택적으로 로컬 주소에서 인증을 비활성화할 수 있습니다.", + "CountIndexersSelected": "{count}개의 인덱서를 선택함", + "Label": "라벨", + "More": "더 보기", + "Donate": "기부하기", + "Menu": "메뉴", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent의 구성된 콘텐츠 레이아웃을 사용할지, 토런트의 원래 레이아웃을 사용할지, 항상 하위 폴더를 생성할지(qBittorrent 4.3.2+)", + "DownloadClientSettingsAddPaused": "일시 중지 추가", + "SecretToken": "비밀 토큰", + "NoDownloadClientsFound": "다운로드 클라이언트를 찾을 수 없음", + "PrioritySettings": "우선 순위: {0}" } diff --git a/src/NzbDrone.Core/Localization/Core/lv.json b/src/NzbDrone.Core/Localization/Core/lv.json index 0967ef424..4fd1aab04 100644 --- a/src/NzbDrone.Core/Localization/Core/lv.json +++ b/src/NzbDrone.Core/Localization/Core/lv.json @@ -1 +1,20 @@ -{} +{ + "About": "Par", + "AcceptConfirmationModal": "Apstiprināt Apstiprināšanas Modālu", + "Actions": "Darbības", + "Add": "Pievienot", + "AddDownloadClient": "Pievienot Lejupielādes Klientu", + "AddConnection": "Pievienot Savienojumu", + "AddConnectionImplementation": "Pievienot Savienojumu - {implementationName}", + "New": "Jauns", + "Connect": "Paziņojumi", + "Notification": "Paziņojumi", + "Notifications": "Paziņojumi", + "EditConnectionImplementation": "Pievienot Savienojumu - {implementationName}", + "EditIndexerImplementation": "Pievienot Nosacījumu - {implementationName}", + "EditApplicationImplementation": "Pievienot Savienojumu - {implementationName}", + "AddIndexerImplementation": "Pievienot Nosacījumu - {implementationName}", + "AddIndexerProxyImplementation": "Pievienot Nosacījumu - {implementationName}", + "AddApplicationImplementation": "Pievienot Savienojumu - {implementationName}", + "EditIndexerProxyImplementation": "Pievienot Nosacījumu - {implementationName}" +} diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index a51be2d64..12e1c4557 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -1,5 +1,5 @@ { - "Add": "Legge til", + "Add": "Legg til", "AddDownloadClient": "Legg til nedlastingsklient", "Added": "La til", "AddingTag": "Legger til tag", @@ -54,7 +54,7 @@ "UI": "Grensesnitt", "ConnectionLost": "Tilkobling mistet", "New": "Ny", - "RSS": "RSS", + "Rss": "RSS", "Applications": "Applikasjoner", "Connections": "Tilkoblinger", "Usenet": "Usenet", @@ -127,13 +127,39 @@ "Artist": "artist", "ApplicationUrlHelpText": "Denne applikasjonens eksterne URL inkludert http(s)://, port og URL base", "ApplyChanges": "Bekreft endringer", - "ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {0} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen", + "ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {length} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen", "ConnectionLostReconnect": "Radarr vil forsøke å koble til automatisk, eller du kan klikke oppdater nedenfor.", "ConnectionLostToBackend": "Radarr har mistet tilkoblingen til baksystemet og må lastes inn på nytt for å gjenopprette funksjonalitet.", "DeleteAppProfileMessageText": "Er du sikker på at du vil slette denne forsinkelsesprofilen?", - "AddConnection": "Legg til kobling", + "AddConnection": "Legg til tilkobling", "AuthBasic": "Grunnleggende (nettleser -popup)", "AuthForm": "Skjemaer (påloggingsside)", "DisabledForLocalAddresses": "Deaktivert for lokale adresser", - "ResetAPIKeyMessageText": "Er du sikker på at du vil tilbakestille API -nøkkelen din?" + "ResetAPIKeyMessageText": "Er du sikker på at du vil tilbakestille API -nøkkelen din?", + "CountApplicationsSelected": "{count} samling(er) valgt", + "Id": "ID", + "UnableToAddANewNotificationPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "Docker": "Docker", + "UnableToAddANewDownloadClientPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "EditApplicationImplementation": "Legg til betingelse - {implementationName}", + "EditIndexerImplementation": "Legg til betingelse - {implementationName}", + "AddApplicationImplementation": "Legg til betingelse - {implementationName}", + "EditConnectionImplementation": "Legg til betingelse - {implementationName}", + "IndexerHDBitsSettingsCodecs": "Kodek", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "Directory": "Mappe", + "UnableToAddANewIndexerPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "AddConnectionImplementation": "Legg til betingelse - {implementationName}", + "AddIndexerImplementation": "Legg til betingelse - {implementationName}", + "AddIndexerProxyImplementation": "Legg til betingelse - {implementationName}", + "UnableToAddANewApplicationPleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "EditIndexerProxyImplementation": "Legg til betingelse - {implementationName}", + "UnableToAddANewAppProfilePleaseTryAgain": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "BuiltIn": "Bygget inn", + "AllSearchResultsHiddenByFilter": "Alle resultatene er skjult av det anvendte filteret", + "AptUpdater": "Bruk apt til å installere oppdateringen", + "Discord": "Discord", + "AddCustomFilter": "Legg til eget filter", + "Clone": "Lukk", + "AddDownloadClientImplementation": "Ny Nedlastingsklient - {implementationName}" } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 57229a7d6..c7c2826d9 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -23,8 +23,8 @@ "Analytics": "Statistieken", "AnalyticsEnabledHelpText": "Stuur anonieme gebruiks- en foutinformatie naar de servers van {appName}. Dit omvat informatie over uw browser, welke {appName} WebUI pagina's u gebruikt, foutrapportage en OS en runtime versie. We zullen deze informatie gebruiken om prioriteiten te stellen voor functies en het verhelpen van fouten.", "ApiKey": "API-sleutel", - "ApiKeyValidationHealthCheckMessage": "Maak je API sleutel alsjeblieft minimaal {0} karakters lang. Dit kan gedaan worden via de instellingen of het configuratiebestand", - "AppDataDirectory": "AppData folder", + "ApiKeyValidationHealthCheckMessage": "Maak je API sleutel alsjeblieft minimaal {length} karakters lang. Dit kan gedaan worden via de instellingen of het configuratiebestand", + "AppDataDirectory": "AppData map", "AppDataLocationHealthCheckMessage": "Updaten zal niet mogelijk zijn om het verwijderen van AppData te voorkomen", "AppProfileInUse": "App-profiel in gebruik", "AppProfileSelectHelpText": "App-profielen worden gebruikt om de instellingen voor RSS, Automatisch zoeken en Interactief zoeken bij applicatiesynchronisatie te beheren", @@ -47,11 +47,11 @@ "Backup": "Veiligheidskopie", "BackupFolderHelpText": "Relatieve paden zullen t.o.v. de {appName} AppData map bekeken worden", "BackupIntervalHelpText": "Tussentijd voor automatische back-up", - "BackupNow": "Nu backup nemen", + "BackupNow": "Back-up nu maken", "BackupRetentionHelpText": "Automatische veiligheidskopieën ouder dan de retentie periode zullen worden opgeruimd", "Backups": "Veiligheidskopieën", "BeforeUpdate": "Voor Update", - "BindAddress": "Aanhaak Adres", + "BindAddress": "Gebonden Adres", "BindAddressHelpText": "Geldig IP-adres, localhost of '*' voor alle interfaces", "Branch": "Branch", "BranchUpdate": "Te gebruiken branch om {appName} bij te werken", @@ -81,7 +81,7 @@ "CouldNotConnectSignalR": "Kan geen verbinding maken met SignalR, gebruikersinterface wordt niet bijgewerkt", "Custom": "Aangepast", "CustomFilters": "Aangepaste Filters", - "DBMigration": "DB Migratie", + "DatabaseMigration": "DB Migratie", "Database": "Databasis", "Date": "Datum", "Dates": "Datum en tijd", @@ -90,15 +90,15 @@ "DeleteApplication": "Applicatie verwijderen", "DeleteApplicationMessageText": "Weet u zeker dat u de applicatie '{0}' wilt verwijderen?", "DeleteBackup": "Verwijder Backup", - "DeleteBackupMessageText": "Bent u zeker dat u de veiligheidskopie '{0}' wilt verwijderen?", + "DeleteBackupMessageText": "Bent u zeker dat u de veiligheidskopie '{name}' wilt verwijderen?", "DeleteDownloadClient": "Verwijder Downloader", - "DeleteDownloadClientMessageText": "Bent u zeker dat u de downloader '{0}' wilt verwijderen?", + "DeleteDownloadClientMessageText": "Bent u zeker dat u de downloader '{name}' wilt verwijderen?", "DeleteIndexerProxy": "Indexeerproxy verwijderen", "DeleteIndexerProxyMessageText": "Weet u zeker dat u de proxy '{0}' wilt verwijderen?", "DeleteNotification": "Verwijder Notificatie", - "DeleteNotificationMessageText": "Bent u zeker dat u de notificatie '{0}' wilt verwijderen?", + "DeleteNotificationMessageText": "Weet je zeker dat je de notificatie ‘{name}’ wil verwijderen?", "DeleteTag": "Verwijder Tag", - "DeleteTagMessageText": "Bent u zeker dat u de tag '{0}' wilt verwijderen?", + "DeleteTagMessageText": "Weet je zeker dat je de tag '{label}' wil verwijderen?", "Description": "Beschrijving", "Details": "Details", "DevelopmentSettings": "Ontwikkelingsinstellingen", @@ -108,19 +108,19 @@ "Donations": "Donaties", "DownloadClient": "Downloader", "DownloadClientSettings": "Downloader Instellingen", - "DownloadClientStatusCheckAllClientMessage": "Alle downloaders zijn onbeschikbaar wegens fouten", - "DownloadClientStatusCheckSingleClientMessage": "Downloaders onbeschikbaar wegens fouten: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "Alle downloaders zijn onbeschikbaar wegens fouten", + "DownloadClientStatusSingleClientHealthCheckMessage": "Downloaders onbeschikbaar wegens fouten: {downloadClientNames}", "DownloadClients": "Downloaders", "DownloadClientsSettingsSummary": "Clientconfiguratie downloaden voor integratie in {appName} UI-zoekopdracht", "Duration": "Duur", "Edit": "Bewerk", "EditIndexer": "Bewerk Indexeerder", "EditSyncProfile": "Synchronisatieprofiel toevoegen", - "Enable": "Activeer", - "EnableAutomaticSearch": "Activeer Automatisch Zoeken", + "Enable": "Inschakelen", + "EnableAutomaticSearch": "Schakel automatisch zoeken in", "EnableAutomaticSearchHelpText": "Zal worden gebruikt wanneer automatische zoekopdrachten worden uitgevoerd via de gebruikersinterface of door {appName}", "EnableIndexer": "Indexer inschakelen", - "EnableInteractiveSearch": "Activeer Interactief Zoeken", + "EnableInteractiveSearch": "Schakel interactief zoeken in", "EnableInteractiveSearchHelpText": "Zal worden gebruikt wanneer interactief zoeken wordt gebruikt", "EnableRss": "RSS inschakelen", "EnableRssHelpText": "Rss-feed voor Indexer inschakelen", @@ -154,7 +154,7 @@ "Grabbed": "Opgehaalde", "Grabs": "Gegrepen", "Health": "Gezondheid", - "HealthNoIssues": "Geen problemen gevonden met uw configuratie", + "NoIssuesWithYourConfiguration": "Geen problemen gevonden met uw configuratie", "HideAdvanced": "Verberg Gevorderd", "History": "Geschiedenis", "HistoryCleanupDaysHelpText": "Zet op 0 om automatisch opschonen uit te schakelen", @@ -170,23 +170,23 @@ "IndexerAuth": "Indexer-authenticatie", "IndexerFlags": "Indexeerder Flags", "IndexerHealthCheckNoIndexers": "Geen indexers ingeschakeld, {appName} geeft geen zoekresultaten terug", - "IndexerLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur: {indexerNames}", "IndexerObsoleteCheckMessage": "Indexeerders zijn verouderd of zijn bijgewerkt: {0}. Gelieve te verwijderen en (of) opnieuw toe te voegen aan {appName}", "IndexerPriority": "Indexeerder Prioriteit", "IndexerPriorityHelpText": "Indexeerder Prioriteit van 1 (Hoogste) tot 50 (Laagste). Standaard: 25.", "IndexerProxies": "Indexer-proxy's", "IndexerProxy": "Indexeerder-proxy", - "IndexerProxyStatusCheckAllClientMessage": "Alle proxy's zijn niet beschikbaar vanwege storingen", - "IndexerProxyStatusCheckSingleClientMessage": "Proxy's niet beschikbaar vanwege storingen: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Alle proxy's zijn niet beschikbaar vanwege storingen", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxy's niet beschikbaar vanwege storingen: {indexerProxyNames}", "IndexerQuery": "Indexeer zoekopdracht", "IndexerRss": "Indexeer RSS", "IndexerSettingsSummary": "Configureer verschillende globale Indexer-instellingen, waaronder proxy's.", - "IndexerStatusCheckAllClientMessage": "Alle indexeerders zijn onbeschikbaar wegens fouten", - "IndexerStatusCheckSingleClientMessage": "Indexeerders onbeschikbaar wegens fouten: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Alle indexeerders zijn onbeschikbaar wegens fouten", + "IndexerStatusUnavailableHealthCheckMessage": "Indexeerders onbeschikbaar wegens fouten: {indexerNames}", "IndexerTagsHelpText": "Gebruik tags om standaardclients op te geven, Indexeerder-proxy's op te geven of gewoon om uw indexeerders te ordenen.", - "IndexerVipCheckExpiredClientMessage": "Indexeerder VIP-voordelen zijn verlopen: {0}", - "IndexerVipCheckExpiringClientMessage": "Indexeerder VIP-voordelen verlopen binnenkort: {0}", + "IndexerVipExpiredHealthCheckMessage": "Indexeerder VIP-voordelen zijn verlopen: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Indexeerder VIP-voordelen verlopen binnenkort: {indexerNames}", "Indexers": "Indexeerders", "Info": "Info", "InstanceName": "Naam van de instantie", @@ -263,17 +263,17 @@ "ProwlarrSupportsAnyIndexer": "{appName} ondersteunt veel indexeerders naast elke indexeerder die de Newznab/Torznab-standaard gebruikt met 'Generic Newznab' (voor usenet) of 'Generic Torznab' (voor torrents). Zoek en selecteer uw indexeerder hieronder.", "Proxy": "Proxy", "ProxyBypassFilterHelpText": "Gebruik ',' als scheidingsteken en '*' als wildcard voor subdomeinen", - "ProxyCheckBadRequestMessage": "Testen van proxy is mislukt. Statuscode: {0}", - "ProxyCheckFailedToTestMessage": "Testen van proxy is mislukt: {0}", - "ProxyCheckResolveIpMessage": "Achterhalen van het IP-adres voor de geconfigureerde proxy host {0} is mislukt", + "ProxyBadRequestHealthCheckMessage": "Testen van proxy is mislukt. Statuscode: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Testen van proxy is mislukt: {url}", + "ProxyResolveIpHealthCheckMessage": "Achterhalen van het IP-adres voor de geconfigureerde proxy host {proxyHostName} is mislukt", "ProxyPasswordHelpText": "Je moet alleen een gebruikersnaam en wachtwoord ingeven als dit vereist is, laat ze anders leeg.", "ProxyType": "Proxy Type", "ProxyUsernameHelpText": "Je moet alleen een gebruikersnaam en wachtwoord ingeven als dit vereist is, laat ze anders leeg.", "Query": "Vraag", "Queue": "Wachtrij", "Queued": "Afwachtend", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "RSS wordt niet ondersteund door deze indexeerder", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS wordt niet ondersteund door deze indexeerder", "ReadTheWikiForMoreInformation": "Lees de Wiki voor meer informatie", "Reddit": "Reddit", "Redirect": "Omleiden", @@ -350,7 +350,7 @@ "SyncLevelAddRemove": "Alleen toevoegen en verwijderen: wanneer het wordt toegevoegd of verwijderd uit {appName}, wordt deze externe app bijgewerkt.", "SyncLevelFull": "Volledige synchronisatie: houdt deze app volledig gesynchroniseerd. Wijzigingen in {appName} worden vervolgens gesynchroniseerd met deze app. Elke wijziging die op afstand wordt aangebracht, wordt bij de volgende synchronisatie overschreven door {appName}.", "System": "Systeem", - "SystemTimeCheckMessage": "De systeemtijd loopt verkeerd met meer dan 1 dag. Geplande taken worden mogelijk niet goed uitgevoerd tot dit is opgelost", + "SystemTimeHealthCheckMessage": "De systeemtijd loopt verkeerd met meer dan 1 dag. Geplande taken worden mogelijk niet goed uitgevoerd tot dit is opgelost", "TableOptions": "Tabel Opties", "TableOptionsColumnsMessage": "Kies welke kolommen zichtbaar zijn en in welke volgorde", "TagCannotBeDeletedWhileInUse": "Kan niet verwijderd worden terwijl in gebruik", @@ -364,7 +364,7 @@ "TestAllApps": "Alle apps testen", "TestAllClients": "Test Alle Downloaders", "TestAllIndexers": "Test Alle Indexeerders", - "TheLatestVersionIsAlreadyInstalled": "De nieuwste versie van {appName} is al geïnstalleerd", + "OnLatestVersion": "De nieuwste versie van {appName} is al geïnstalleerd", "Time": "Tijd", "Title": "Titel", "Today": "Vandaag", @@ -386,9 +386,9 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "Kan geen nieuwe Indexeerder-proxy toevoegen. Probeer het opnieuw.", "UnableToAddANewNotificationPleaseTryAgain": "Kon geen nieuwe notificatie toevoegen, gelieve opnieuw te proberen.", "UnableToLoadAppProfiles": "Kan app-profielen niet laden", - "UnableToLoadBackups": "Kon geen veiligheidskopieën laden", + "BackupsLoadError": "Kon geen veiligheidskopieën laden", "UnableToLoadDevelopmentSettings": "Kan ontwikkelingsinstellingen niet laden", - "UnableToLoadDownloadClients": "Downloaders kunnen niet worden geladen", + "DownloadClientsLoadError": "Downloaders kunnen niet worden geladen", "UnableToLoadGeneralSettings": "Kon Algemene instellingen niet inladen", "UnableToLoadHistory": "Kon geschiedenis niet laden", "UnableToLoadIndexerProxies": "Kan Indexeerder-proxy's niet laden", @@ -399,10 +399,10 @@ "UnsavedChanges": "Onopgeslagen Wijzigingen", "UnselectAll": "Alles Deselecteren", "UpdateAutomaticallyHelpText": "Download en installeer updates automatisch. Je zal nog steeds kunnen installeren vanuit Systeem: Updates", - "UpdateCheckStartupNotWritableMessage": "Kan de update niet installeren omdat de map '{0}' niet schrijfbaar is voor de gebruiker '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Kan de update niet installeren omdat de map '{0}' zich in een 'App Translocation' map bevindt.", - "UpdateCheckUINotWritableMessage": "Kan de update niet installeren omdat de UI map '{0}' niet schrijfbaar is voor de gebruiker '{1}'.", - "UpdateMechanismHelpText": "Gebruik het ingebouwde updatemechanisme of een extern script", + "UpdateStartupNotWritableHealthCheckMessage": "Kan de update niet installeren omdat de map '{startupFolder}' niet schrijfbaar is voor de gebruiker '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Kan de update niet installeren omdat de map '{startupFolder}' zich in een 'App Translocation' map bevindt.", + "UpdateUiNotWritableHealthCheckMessage": "Kan de update niet installeren omdat de UI map '{uiFolder}' niet schrijfbaar is voor de gebruiker '{userName}'.", + "UpdateMechanismHelpText": "Gebruik de ingebouwde updater van {appName} of een script", "UpdateScriptPathHelpText": "Pad naar een aangepast script dat een uitgepakt updatepakket accepteert en de rest van het updateproces afhandelt", "Updates": "Updates", "Uptime": "Bedrijfstijd", @@ -425,7 +425,7 @@ "ApplyTagsHelpTextHowToApplyIndexers": "Hoe tags toepassen op de geselecteerde indexeerders", "ApplyTagsHelpTextRemove": "Verwijderen: Verwijder de ingevoerde tags", "ApplyTagsHelpTextReplace": "Vervangen: Vervang de tags met de ingevoerde tags (vul geen tags in om alle tags te wissen)", - "CountIndexersSelected": "{0} Indexer(s) Geselecteerd", + "CountIndexersSelected": "{count} Indexer(s) Geselecteerd", "DeleteSelectedApplicationsMessageText": "Bent u zeker dat u de indexeerder '{0}' wilt verwijderen?", "DeleteSelectedDownloadClients": "Verwijder Downloader", "DeleteSelectedDownloadClientsMessageText": "Bent u zeker dat u de indexeerder '{0}' wilt verwijderen?", @@ -438,7 +438,7 @@ "DownloadClientPriorityHelpText": "Geef prioriteit aan meerdere downloaders. Round-Robin wordt gebruikt voor downloaders met dezelfde prioriteit.", "Genre": "Genres", "Year": "Jaar", - "UpdateAvailable": "Nieuwe update is beschikbaar", + "UpdateAvailableHealthCheckMessage": "Nieuwe update is beschikbaar", "Label": "Label", "Publisher": "Uitgever", "ApplyChanges": "Pas Wijzigingen Toe", @@ -452,13 +452,13 @@ "NotificationStatusSingleClientHealthCheckMessage": "Applicaties onbeschikbaar door fouten", "AddConnection": "Voeg connectie toe", "NotificationStatusAllClientHealthCheckMessage": "Alle applicaties onbeschikbaar door fouten", - "AuthBasic": "Basis (Browser Pop-up)", - "AuthForm": "Formulier (Login Pagina)", + "AuthBasic": "Basic (Browser Pop-up)", + "AuthForm": "Formulier (inlogpagina)", "DisabledForLocalAddresses": "Uitgeschakeld voor lokale adressen", "None": "Geen", "ResetAPIKeyMessageText": "Bent u zeker dat u uw API-sleutel wilt resetten?", "AddConnectionImplementation": "Voeg connectie toe - {implementationName}", - "AddDownloadClientImplementation": "Voeg Downloadclient toe - {implementationName}", + "AddDownloadClientImplementation": "Voeg Downloadclient Toe - {implementationName}", "AddIndexerImplementation": "Indexeerder toevoegen - {implementationName}", "AdvancedSettingsHiddenClickToShow": "Geavanceerde instellingen zijn verborgen, klik om te tonen", "AdvancedSettingsShownClickToHide": "Geavanceerde instellingen worden getoond, klik om te verbergen", @@ -473,5 +473,44 @@ "AddApplicationImplementation": "Voeg connectie toe - {implementationName}", "AddIndexerProxyImplementation": "Indexeerder toevoegen - {implementationName}", "EditApplicationImplementation": "Voeg connectie toe - {implementationName}", - "EditIndexerProxyImplementation": "Indexeerder toevoegen - {implementationName}" + "EditIndexerProxyImplementation": "Indexeerder toevoegen - {implementationName}", + "AuthenticationMethod": "Authenticatiemethode", + "AuthenticationRequired": "Verificatie vereist", + "AuthenticationMethodHelpTextWarning": "Selecteer een geldige verificatie methode", + "AuthenticationRequiredHelpText": "Pas aan welke requests verificatie nodig hebben. Pas niets aan als je de risico's niet begrijpt.", + "AuthenticationRequiredPasswordHelpTextWarning": "Voer een nieuw wachtwoord in", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Bevestig het nieuwe wachtwoord", + "AuthenticationRequiredUsernameHelpTextWarning": "Voeg een nieuwe gebruikersnaam in", + "AuthenticationRequiredWarning": "Om toegang zonder authenticatie te voorkomen vereist {appName} nu verificatie. Je kan dit optioneel uitschakelen voor lokale adressen.", + "Episode": "aflevering", + "CountApplicationsSelected": "{count} Collectie(s) geselecteerd", + "BlackholeFolderHelpText": "De map waarin {appName} het {extension} bestand opslaat", + "GrabRelease": "Uitgave Ophalen", + "PrioritySettings": "Prioriteit: {priority}", + "Artist": "artiest", + "IndexerHDBitsSettingsMediums": "Gemiddeld", + "IndexerHDBitsSettingsCodecs": "Codec", + "Directory": "Map", + "ProxyValidationBadRequest": "Testen van proxy is mislukt. Statuscode: {statusCode}", + "CustomFilter": "Aangepaste Filters", + "Any": "Elke", + "BuiltIn": "Ingebouwd", + "Script": "Script", + "PublishedDate": "Publicatie Datum", + "Redirected": "Omleiden", + "AllSearchResultsHiddenByFilter": "Alle resultaten zijn verborgen door het toegepaste filter", + "Clone": "Kloon", + "DownloadClientSettingsUrlBaseHelpText": "Voegt een voorvoegsel toe aan de {connectionName} url, zoals {url}", + "AptUpdater": "Gebruik apt om de update te installeren", + "DockerUpdater": "Update de docker container om de update te ontvangen", + "ErrorRestoringBackup": "Fout bij het herstellen van de back-up", + "ExternalUpdater": "{appName} is geconfigureerd om een extern update mechanisme te gebruiken", + "NoEventsFound": "Geen gebeurtenissen gevonden", + "RestartReloadNote": "Aantekening: {appName} zal automatisch de Ui herstarten en herladen gedurende het herstel proces.", + "UpdateAppDirectlyLoadError": "Kan {appName} niet rechtstreeks updaten,", + "WouldYouLikeToRestoreBackup": "Wilt u de back-up {name} herstellen?", + "Download": "Downloaden", + "InstallLatest": "Installeer Nieuwste Versie", + "CurrentlyInstalled": "Momenteel Geïnstalleerd", + "Mixed": "Opgelost" } diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 163d50328..a1e861845 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -6,7 +6,7 @@ "Analytics": "Analityka", "All": "Wszystkie", "Added": "Dodane", - "Actions": "Aktywności", + "Actions": "Akcje", "About": "O", "Logging": "Logowanie", "EnableSslHelpText": " Wymaga ponownego uruchomienia jako administrator, aby odniosło skutek", @@ -22,7 +22,7 @@ "Automatic": "Automatyczny", "ApplyTags": "Zastosuj tagi", "BackupIntervalHelpText": "Odstęp czasu między automatycznymi kopiami zapasowymi", - "Close": "Blisko", + "Close": "Zamknij", "CloseCurrentModal": "Zamknij bieżący tryb", "Delete": "Usunąć", "Discord": "Niezgoda", @@ -54,7 +54,7 @@ "ApiKey": "Klucz API", "UI": "UI", "AcceptConfirmationModal": "Zaakceptuj tryb potwierdzenia", - "AddIndexer": "Dodaj indeksator", + "AddIndexer": "Dodaj indekser", "AddingTag": "Dodawanie tagu", "Age": "Wiek", "CertificateValidationHelpText": "Zmień ścisłą walidację certyfikatu HTTPS", @@ -73,7 +73,7 @@ "CouldNotConnectSignalR": "Nie można połączyć się z SignalR, interfejs użytkownika nie zostanie zaktualizowany", "EnableInteractiveSearchHelpText": "Będzie używany, gdy używane jest wyszukiwanie interaktywne", "HomePage": "Strona główna", - "IndexerProxyStatusCheckSingleClientMessage": "Listy niedostępne z powodu błędów: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Listy niedostępne z powodu błędów: {indexerProxyNames}", "LastWriteTime": "Czas ostatniego zapisu", "Level": "Poziom", "LogFiles": "Pliki dziennika", @@ -86,13 +86,13 @@ "PageSize": "Rozmiar strony", "PageSizeHelpText": "Liczba elementów do pokazania na każdej stronie", "PendingChangesStayReview": "Zostań i przejrzyj zmiany", - "RSSIsNotSupportedWithThisIndexer": "RSS nie jest obsługiwany przez ten indeksator", + "RssIsNotSupportedWithThisIndexer": "RSS nie jest obsługiwany przez ten indeksator", "Settings": "Ustawienia", "ShowAdvanced": "Pokaż zaawansowane", "Test": "Test", "UILanguage": "Język interfejsu użytkownika", "UnableToLoadNotifications": "Nie można załadować powiadomień", - "UpdateCheckUINotWritableMessage": "Nie można zainstalować aktualizacji, ponieważ użytkownik „{1}” nie ma prawa zapisu w folderze interfejsu użytkownika „{0}”.", + "UpdateUiNotWritableHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ użytkownik „{userName}” nie ma prawa zapisu w folderze interfejsu użytkownika „{uiFolder}”.", "UseProxy": "Użyj proxy", "DeleteIndexerProxyMessageText": "Czy na pewno chcesz usunąć tag „{0}”?", "DeleteNotificationMessageText": "Czy na pewno chcesz usunąć powiadomienie „{0}”?", @@ -109,7 +109,7 @@ "AuthenticationMethodHelpText": "Wymagaj nazwy użytkownika i hasła, aby uzyskać dostęp do {appName}", "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 IP4 lub „*” dla wszystkich interfejsów", + "BindAddressHelpText": "Prawidłowy adres IP, localhost lub '*' dla wszystkich interfejsów", "BranchUpdateMechanism": "Gałąź używana przez zewnętrzny mechanizm aktualizacji", "BypassProxyForLocalAddresses": "Pomijaj serwer proxy dla adresów lokalnych", "CancelPendingTask": "Czy na pewno chcesz anulować to oczekujące zadanie?", @@ -121,21 +121,21 @@ "Custom": "Zwyczaj", "Date": "Data", "Dates": "Daktyle", - "DBMigration": "Migracja bazy danych", + "DatabaseMigration": "Migracja bazy danych", "DeleteApplicationMessageText": "Czy na pewno chcesz usunąć powiadomienie „{0}”?", "DeleteBackup": "Usuń kopię zapasową", - "DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{0}”?", + "DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{name}”?", "DeleteDownloadClient": "Usuń klienta pobierania", - "DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{0}”?", + "DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{name}”?", "DeleteNotification": "Usuń powiadomienie", "Disabled": "Wyłączone", "Docker": "Doker", - "DownloadClientStatusCheckSingleClientMessage": "Klienci pobierania niedostępni z powodu błędów: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Klienci pobierania niedostępni z powodu błędów: {downloadClientNames}", "EditIndexer": "Edytuj indeksator", "Enable": "Włączyć", "EnableAutomaticSearch": "Włącz automatyczne wyszukiwanie", "Enabled": "Włączone", - "DownloadClientStatusCheckAllClientMessage": "Wszyscy klienci pobierania są niedostępni z powodu błędów", + "DownloadClientStatusAllClientHealthCheckMessage": "Wszyscy klienci pobierania są niedostępni z powodu błędów", "EnableInteractiveSearch": "Włącz wyszukiwanie interaktywne", "EventType": "Typ wydarzenia", "Failed": "Niepowodzenie", @@ -150,19 +150,19 @@ "GeneralSettings": "Ustawienia główne", "GeneralSettingsSummary": "Port, SSL, nazwa użytkownika / hasło, proxy, analizy i aktualizacje", "Grabbed": "Złapał", - "HealthNoIssues": "Żadnych problemów z konfiguracją", + "NoIssuesWithYourConfiguration": "Żadnych problemów z konfiguracją", "HideAdvanced": "Ukryj zaawansowane", "History": "Historia", "Hostname": "Nazwa hosta", "IgnoredAddresses": "Ignorowane adresy", "IllRestartLater": "Zrestartuję później", - "IndexerLongTermStatusCheckAllClientMessage": "Wszystkie indeksatory są niedostępne z powodu awarii przez ponad 6 godzin", - "IndexerLongTermStatusCheckSingleClientMessage": "Indeksatory niedostępne z powodu błędów przez ponad 6 godzin: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Wszystkie indeksatory są niedostępne z powodu awarii przez ponad 6 godzin", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indeksatory niedostępne z powodu błędów przez ponad 6 godzin: {indexerNames}", "IndexerPriority": "Priorytet indeksatora", "IndexerPriorityHelpText": "Priorytet indeksatora od 1 (najwyższy) do 50 (najniższy). Domyślnie: 25.", - "IndexerProxyStatusCheckAllClientMessage": "Wszystkie listy są niedostępne z powodu błędów", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Wszystkie listy są niedostępne z powodu błędów", "ShowSearchHelpText": "Pokaż przycisk wyszukiwania po najechaniu kursorem", - "IndexerStatusCheckSingleClientMessage": "Indeksatory niedostępne z powodu błędów: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Indeksatory niedostępne z powodu błędów: {indexerNames}", "Info": "Informacje", "Interval": "Interwał", "Language": "Język", @@ -194,9 +194,9 @@ "Priority": "Priorytet", "Proxy": "Pełnomocnik", "ProxyBypassFilterHelpText": "Użyj znaku „,” jako separatora i „*”. jako symbol wieloznaczny dla subdomen", - "ProxyCheckBadRequestMessage": "Nie udało się przetestować serwera proxy. StatusCode: {0}", - "ProxyCheckFailedToTestMessage": "Nie udało się przetestować serwera proxy: {0}", - "ProxyCheckResolveIpMessage": "Nie udało się rozwiązać adresu IP dla skonfigurowanego hosta proxy {0}", + "ProxyBadRequestHealthCheckMessage": "Nie udało się przetestować serwera proxy. StatusCode: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Nie udało się przetestować serwera proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Nie udało się rozwiązać adresu IP dla skonfigurowanego hosta proxy {proxyHostName}", "ProxyPasswordHelpText": "Musisz tylko wprowadzić nazwę użytkownika i hasło, jeśli jest to wymagane. W przeciwnym razie pozostaw je puste.", "ProxyUsernameHelpText": "Musisz tylko wprowadzić nazwę użytkownika i hasło, jeśli jest to wymagane. W przeciwnym razie pozostaw je puste.", "Queue": "Kolejka", @@ -237,10 +237,10 @@ "SSLCertPathHelpText": "Ścieżka do pliku pfx", "SSLPort": "Port SSL", "StartTypingOrSelectAPathBelow": "Zacznij pisać lub wybierz ścieżkę poniżej", - "StartupDirectory": "Katalog startowy", + "StartupDirectory": "Katalog Startowy", "Status": "Status", "SuggestTranslationChange": "Zaproponuj zmianę tłumaczenia", - "SystemTimeCheckMessage": "Czas systemowy jest wyłączony o więcej niż 1 dzień. Zaplanowane zadania mogą nie działać poprawnie, dopóki czas nie zostanie skorygowany", + "SystemTimeHealthCheckMessage": "Czas systemowy jest wyłączony o więcej niż 1 dzień. Zaplanowane zadania mogą nie działać poprawnie, dopóki czas nie zostanie skorygowany", "Tags": "Tagi", "TagsHelpText": "Dotyczy filmów z co najmniej jednym pasującym tagiem", "TagsSettingsSummary": "Zobacz wszystkie tagi i sposób ich używania. Nieużywane tagi można usunąć", @@ -261,15 +261,15 @@ "UnableToAddANewIndexerPleaseTryAgain": "Nie można dodać nowego indeksatora, spróbuj ponownie.", "UnableToAddANewIndexerProxyPleaseTryAgain": "Nie można dodać nowego indeksatora, spróbuj ponownie.", "UnableToAddANewNotificationPleaseTryAgain": "Nie można dodać nowego powiadomienia, spróbuj ponownie.", - "UnableToLoadBackups": "Nie można załadować kopii zapasowych", - "UnableToLoadDownloadClients": "Nie można załadować klientów pobierania", + "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", "UnsavedChanges": "Niezapisane zmiany", "UpdateAutomaticallyHelpText": "Automatycznie pobieraj i instaluj aktualizacje. Nadal będziesz mógł zainstalować z System: Updates", - "UpdateCheckStartupNotWritableMessage": "Nie można zainstalować aktualizacji, ponieważ użytkownik „{1}” nie ma prawa zapisu do folderu startowego „{0}”.", - "UpdateCheckStartupTranslocationMessage": "Nie można zainstalować aktualizacji, ponieważ folder startowy „{0}” znajduje się w folderze translokacji aplikacji.", + "UpdateStartupNotWritableHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ użytkownik „{userName}” nie ma prawa zapisu do folderu startowego „{startupFolder}”.", + "UpdateStartupTranslocationHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ folder startowy „{startupFolder}” znajduje się w folderze translokacji aplikacji.", "UpdateMechanismHelpText": "Użyj wbudowanego aktualizatora {appName} lub skryptu", "Updates": "Aktualizacje", "UpdateScriptPathHelpText": "Ścieżka do niestandardowego skryptu, który pobiera wyodrębniony pakiet aktualizacji i obsługuje pozostałą część procesu aktualizacji", @@ -302,9 +302,9 @@ "Columns": "Kolumny", "Host": "Gospodarz", "Indexers": "Indeksatory", - "IndexerStatusCheckAllClientMessage": "Wszystkie indeksatory są niedostępne z powodu błędów", + "IndexerStatusAllUnavailableHealthCheckMessage": "Wszystkie indeksatory są niedostępne z powodu błędów", "RestartNow": "Zrestartuj teraz", - "RSS": "RSS", + "Rss": "RSS", "Tasks": "Zadania", "MaintenanceRelease": "Wersja konserwacyjna: poprawki błędów i inne ulepszenia. Aby uzyskać więcej informacji, zobacz historię zatwierdzeń na Github", "HistoryCleanupDaysHelpTextWarning": "Pliki w koszu starsze niż wybrana liczba dni zostaną automatycznie wyczyszczone", @@ -341,7 +341,7 @@ "ApplicationLongTermStatusCheckAllClientMessage": "Wszystkie indeksatory są niedostępne z powodu awarii przez ponad 6 godzin", "Remove": "Usunąć", "Replace": "Zastąpić", - "TheLatestVersionIsAlreadyInstalled": "Najnowsza wersja {appName} jest już zainstalowana", + "OnLatestVersion": "Najnowsza wersja {appName} jest już zainstalowana", "ApplicationURL": "Link do aplikacji", "ApplicationUrlHelpText": "Zewnętrzny URL tej aplikacji zawierający http(s)://, port i adres URL", "ApplyTagsHelpTextAdd": "Dodaj: dodaj tagi do istniejącej listy tagów", @@ -357,17 +357,17 @@ "DeleteSelectedIndexersMessageText": "Czy na pewno chcesz usunąć indeksator „{0}”?", "DownloadClientPriorityHelpText": "Nadaj priorytet wielu klientom pobierania. W przypadku klientów o tym samym priorytecie używane jest działanie okrężne.", "Track": "Ślad", - "UpdateAvailable": "Dostępna jest aktualizacja", + "UpdateAvailableHealthCheckMessage": "Dostępna jest aktualizacja", "Genre": "Gatunki", "ApplyChanges": "Zastosuj zmiany", - "ApiKeyValidationHealthCheckMessage": "Zaktualizuj swój klucz API aby był długi na co najmniej {0} znaków. Możesz to zrobić poprzez ustawienia lub plik konfiguracyjny", + "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 jakości '{0}'?", "ConnectionLostReconnect": "Radarr spróbuje połączyć się automatycznie lub możesz kliknąć przycisk przeładuj poniżej.", "RecentChanges": "Ostatnie zmiany", "WhatsNew": "Co nowego?", "ConnectionLostToBackend": "Radarr utracił połączenie z silnikiem programu, aby przywrócić funkcjonalność musi zostać zrestartowany.", "minutes": "Minuty", - "NotificationStatusSingleClientHealthCheckMessage": "Listy niedostępne z powodu błędów: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Listy niedostępne z powodu błędów: {notificationNames}", "AddConnection": "Edytuj kolekcję", "NotificationStatusAllClientHealthCheckMessage": "Wszystkie listy są niedostępne z powodu błędów", "AuthForm": "Formularze (strona logowania)", @@ -375,5 +375,58 @@ "None": "Żaden", "AuthBasic": "Podstawowe (wyskakujące okienko przeglądarki)", "ResetAPIKeyMessageText": "Czy na pewno chcesz zresetować swój klucz API?", - "RestartProwlarr": "Zrestartuj {appName}" + "RestartProwlarr": "Zrestartuj {appName}", + "AddConnectionImplementation": "Dodaj Connection - {implementationName}", + "AddDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}", + "CountApplicationsSelected": "Wybrane kolekcje: {0}", + "EditDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}", + "Id": "Identyfikator", + "AddApplicationImplementation": "Dodaj Connection - {implementationName}", + "AddIndexerImplementation": "Dodaj indeks - {implementationName}", + "AddIndexerProxyImplementation": "Dodaj condition - {implementationName}", + "EditConnectionImplementation": "Dodaj Connection - {implementationName}", + "EditApplicationImplementation": "Dodaj Connection - {implementationName}", + "EditIndexerImplementation": "Dodaj condition - {implementationName}", + "IndexerHDBitsSettingsMediums": "Średni", + "EditIndexerProxyImplementation": "Dodaj condition - {implementationName}", + "Directory": "Folder", + "IndexerHDBitsSettingsCodecs": "Kodek", + "ProxyValidationBadRequest": "Nie udało się przetestować serwera proxy. StatusCode: {statusCode}", + "CustomFilter": "Filtry niestandardowe", + "GrabRelease": "Pobierz Wydanie", + "Script": "Scenariusz", + "BuiltIn": "Wbudowany", + "PublishedDate": "Data publikacji", + "AllSearchResultsHiddenByFilter": "Wszystkie wyniki są ukrywane przez zastosowany filtr", + "AppUpdated": "{appName} Zaktualizowany", + "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ę autoryzacji", + "Any": "Dowolny", + "AuthenticationMethod": "Metoda Autoryzacji", + "AuthenticationRequired": "Wymagana Autoryzacja", + "Categories": "Kategorie", + "Label": "Etykieta", + "Notification": "Powiadomienia", + "Season": "Sezon", + "Theme": "Motyw", + "Artist": "artysta", + "Album": "album", + "Connect": "Powiadomienia", + "Episode": "odcinek", + "Notifications": "Powiadomienia", + "Publisher": "Wydawca", + "Download": "Ściągnij", + "ErrorRestoringBackup": "Błąd podczas przywracania kopii zapasowej", + "ExternalUpdater": "{appName} jest skonfigurowany do korzystania z zewnętrznego mechanizmu aktualizacji", + "NoEventsFound": "Nie znaleziono wydarzeń", + "RestartReloadNote": "Uwaga: {appName} automatycznie uruchomi się ponownie i przeładuje interfejs użytkownika podczas procesu przywracania.", + "UpdateAppDirectlyLoadError": "Nie można bezpośrednio zaktualizować {appName},", + "AptUpdater": "Użyj apt, aby zainstalować aktualizację", + "DockerUpdater": "zaktualizuj kontener Dockera, aby otrzymać aktualizację", + "InstallLatest": "Zainstaluj najnowsze", + "Clone": "Zamknij", + "Stats": "Status", + "CurrentlyInstalled": "Aktualnie zainstalowane", + "Mixed": "Naprawiony" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 00639373d..6a49c3d2c 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -4,9 +4,9 @@ "Warn": "Avisar", "View": "Ver", "Updates": "Atualizações", - "UpdateCheckUINotWritableMessage": "Não é possível instalar a atualização porque a pasta da IU \"{0}\" não tem permissões de escrita para o utilizador \"{1}\".", - "UpdateCheckStartupNotWritableMessage": "Não é possível instalar a atualização porque a pasta de arranque \"{0}\" não tem permissões de escrita para o utilizador \"{1}\".", - "UpdateCheckStartupTranslocationMessage": "Não é possível instalar a atualização porque a pasta de arranque \"{0}\" está em uma pasta de transposição de aplicações.", + "UpdateUiNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta da IU \"{uiFolder}\" não tem permissões de escrita para o utilizador \"{userName}\".", + "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de arranque \"{startupFolder}\" não tem permissões de escrita para o utilizador \"{userName}\".", + "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de arranque \"{startupFolder}\" está em uma pasta de transposição de aplicações.", "UnselectAll": "Desmarcar todos", "UnsavedChanges": "Mudanças não guardadas", "UISettingsSummary": "Opções de calendário, data e modo de daltonismo", @@ -21,7 +21,7 @@ "Tags": "Etiquetas", "TableOptionsColumnsMessage": "Escolha quais colunas são visíveis e em qual ordem aparecem", "TableOptions": "Opções da tabela", - "SystemTimeCheckMessage": "A hora do sistema está atrasada em mais de 1 dia. As tarefas agendadas podem não ocorrer corretamente até a hora ser corrigida", + "SystemTimeHealthCheckMessage": "A hora do sistema está atrasada em mais de 1 dia. As tarefas agendadas podem não ocorrer corretamente até a hora ser corrigida", "System": "Sistema", "Style": "Estilo", "Status": "Estado", @@ -55,9 +55,9 @@ "ReleaseBranchCheckOfficialBranchMessage": "A ramificação {0} não é uma ramificação de versões válida do {appName}, você não receberá atualizações", "Refresh": "Atualizar", "Queue": "Fila", - "ProxyCheckResolveIpMessage": "Não é possível resolver o Endereço IP para o Anfitrião de proxy {0} definido", - "ProxyCheckFailedToTestMessage": "Falha ao testar o proxy: {0}", - "ProxyCheckBadRequestMessage": "Falha ao testar o proxy. Código de estado: {0}", + "ProxyResolveIpHealthCheckMessage": "Não é possível resolver o Endereço IP para o Anfitrião de proxy {proxyHostName} definido", + "ProxyFailedToTestHealthCheckMessage": "Falha ao testar o proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Falha ao testar o proxy. Código de estado: {statusCode}", "Proxy": "Proxy", "Protocol": "Protocolo", "PendingChangesStayReview": "Ficar e rever mudanças", @@ -79,14 +79,14 @@ "Language": "Idioma", "KeyboardShortcuts": "Atalhos do teclado", "Info": "Informações", - "IndexerStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a falhas: {0}", - "IndexerStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a falhas", + "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", "Indexers": "Indexadores", "Indexer": "Indexador", "Host": "Anfitrião", "History": "Histórico", "HideAdvanced": "Ocultar avançado", - "HealthNoIssues": "Não há problemas com suas definições", + "NoIssuesWithYourConfiguration": "Não há problemas com suas definições", "Health": "Estado de funcionamento", "Grabbed": "Capturado", "GeneralSettingsSummary": "Porta, SSL, nome de utilizador/palavra-passe, proxy, análises e actualizações", @@ -100,8 +100,8 @@ "Events": "Eventos", "Error": "Erro", "Edit": "Editar", - "DownloadClientStatusCheckSingleClientMessage": "Clientes de transferências indisponíveis devido a falhas: {0}", - "DownloadClientStatusCheckAllClientMessage": "Todos os clientes de transferências estão indisponíveis devido a falhas", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de transferências indisponíveis devido a falhas: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Todos os clientes de transferências estão indisponíveis devido a falhas", "DownloadClientsSettingsSummary": "Definições do cliente de transferências para integração à pesquisa da IU do {appName}", "DownloadClients": "Clientes de transferências", "DownloadClient": "Cliente de transferências", @@ -120,7 +120,7 @@ "Clear": "Limpar", "Cancel": "Cancelar", "BackupNow": "Criar cópia de segurança", - "Backup": "Cópia de segurança", + "Backup": "Backup", "Apply": "Aplicar", "Analytics": "Análises", "All": "Todos", @@ -132,7 +132,7 @@ "AuthenticationMethodHelpText": "Solicitar nome de utilizador e palavra-passe para acessar ao {appName}", "Authentication": "Autenticação", "ApplyTags": "Aplicar etiquetas", - "AppDataDirectory": "Pasta AppData", + "AppDataDirectory": "Diretório AppData", "ApiKey": "Chave da API", "AnalyticsEnabledHelpText": "Envia informações anônimas de uso e de erros aos servidores do {appName}. Isso inclui informações sobre seu browser, páginas utilizadas na WebUI do {appName}, relatórios de erros, bem como as versões do sistema operativo e da aplicação. Utilizaremos essas informações para priorizar funcionalidades e correções de bugs.", "ProxyType": "Tipo de proxy", @@ -170,7 +170,7 @@ "DeleteNotification": "Eliminar notificação", "DeleteDownloadClient": "Eliminar cliente de transferências", "DeleteBackup": "Eliminar cópia de segurança", - "DBMigration": "Migração da base de dados", + "DatabaseMigration": "Migração da base de dados", "ConnectSettings": "Definições de ligação", "CloneProfile": "Clonar perfil", "ChangeHasNotBeenSavedYet": "A mudança ainda não foi guardada", @@ -181,7 +181,7 @@ "BindAddressHelpText": "Endereço de IP válido, localhost ou \"*\" para todas as interfaces", "BindAddress": "Endereço de vínculo", "Backups": "Cópias de segurança", - "BackupFolderHelpText": "Caminhos relativos estarão na pasta AppData do {appName}", + "BackupFolderHelpText": "Caminhos relativos estarão no diretório AppData do {appName}", "BackupIntervalHelpText": "Intervalo entre cópias de segurança automáticas", "BackupRetentionHelpText": "Cópias de segurança automáticas anteriores ao período de retenção serão eliminadas automaticamente", "ClientPriority": "Prioridade do cliente", @@ -192,7 +192,7 @@ "AddingTag": "A adicionar etiqueta", "AutomaticSearch": "Pesquisa automática", "UnableToAddANewIndexerPleaseTryAgain": "Não foi possível adicionar um novo indexador, tenta novamente.", - "RSSIsNotSupportedWithThisIndexer": "RSS não é suportado por esse indexador", + "RssIsNotSupportedWithThisIndexer": "RSS não é suportado por esse indexador", "ProwlarrSupportsAnyIndexer": "O {appName} suporta vários indexadores, além de qualquer indexador que usa o padrão Newznab/Torznab como \"Newznab genérico\" (para Usenet) e \"Torznab genérico\" (para torrents). Pesquise e selecione os indexadores na listagem abaixo.", "IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25.", "IndexerPriority": "Prioridade do indexador", @@ -268,9 +268,9 @@ "UnableToLoadNotifications": "Não foi possível carregar as notificações", "UnableToLoadHistory": "Não foi possível carregar o histórico", "UnableToLoadGeneralSettings": "Não foi possível carregar as definições gerais", - "UnableToLoadDownloadClients": "Não foi possível carregar os clientes de transferências", + "DownloadClientsLoadError": "Não foi possível carregar os clientes de transferências", "UnableToAddANewDownloadClientPleaseTryAgain": "Não foi possível adicionar um novo cliente de transferências, tenta novamente.", - "UnableToLoadBackups": "Não foi possível carregar as cópias de segurança", + "BackupsLoadError": "Não foi possível carregar as cópias de segurança", "UnableToAddANewNotificationPleaseTryAgain": "Não foi possível adicionar uma nova notificação, tenta novamente.", "UISettings": "Definições da IU", "UILanguageHelpTextWarning": "É preciso reiniciar o browser", @@ -289,8 +289,8 @@ "IndexerRss": "RSS do indexador", "IndexerQuery": "Consulta do indexador", "IndexerObsoleteCheckMessage": "Os seguintes indexadores são obsoletos ou foram actualizados: {0}. Remova-os e/ou adicione-os novamente ao {appName}", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a erros por mais de 6 horas: {0}", - "IndexerLongTermStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a erros por mais de 6 horas", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a erros por mais de 6 horas: {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a erros por mais de 6 horas", "IndexerHealthCheckNoIndexers": "Não há indexadores activados, o {appName} não retornará resultados de pesquisa", "IndexerAuth": "Autenticação do indexador", "EnableRssHelpText": "Activar feed RSS para o indexador", @@ -322,7 +322,7 @@ "SettingsIndexerLoggingHelpText": "Registar dados adicionais do indexador, incluindo resposta", "SettingsIndexerLogging": "Registo em log avançado do indexador", "SettingsFilterSentryEvents": "Filtrar eventos de análise", - "RSS": "RSS", + "Rss": "RSS", "RedirectHelpText": "Redireccionar as solicitações de transferência de entrada para o indexador e obter diretamente, em vez de por proxy usando o {appName}", "Redirect": "Redireccionar", "Reddit": "Reddit", @@ -365,9 +365,9 @@ "DeleteIndexerProxyMessageText": "Tem a certeza que quer eliminar o proxy \"{name}\"?", "IndexerProxy": "Proxy de Indexador", "AddIndexerProxy": "Adicionar Proxy de Indexador", - "IndexerProxyStatusCheckSingleClientMessage": "Proxys indisponíveis devido a falhas: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxys indisponíveis devido a falhas: {indexerProxyNames}", "IndexerProxies": "Proxys de Indexadores", - "IndexerProxyStatusCheckAllClientMessage": "Todos os proxys estão indisponíveis devido a falhas", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Todos os proxys estão indisponíveis devido a falhas", "NoLinks": "Sem ligações", "UnableToAddANewIndexerProxyPleaseTryAgain": "Não foi possível adicionar um novo indexador, tenta novamente.", "Filters": "Filtros", @@ -390,7 +390,7 @@ "InstanceName": "Nome da Instancia", "InstanceNameHelpText": "Nome da instância na aba e nome da aplicação para Syslog", "UnableToLoadIndexerProxies": "Incapaz de ler o indexador de proxies", - "UnableToLoadApplicationList": "Não foi possível carregar a lista de aplicações", + "ApplicationsLoadError": "Não foi possível carregar a lista de aplicações", "ApplicationLongTermStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a erros á mais de 6 horas", "ApplicationLongTermStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a erros à mais de 6 horas: {0}", "Duration": "Duração", @@ -404,11 +404,11 @@ "Queued": "Em fila", "Remove": "Remover", "Replace": "Substituir", - "TheLatestVersionIsAlreadyInstalled": "A versão mais recente do {appName} já está instalada", + "OnLatestVersion": "A versão mais recente do {appName} já está instalada", "AddSyncProfile": "Adicionar Perfil de Sincronização", "AddApplication": "Adicionar Aplicação", "AddCustomFilter": "Adicionar Filtro customizado", - "ApiKeyValidationHealthCheckMessage": "Por favor, actualize a sua API Key para ter no minimo {0} caracteres. Pode fazer através das definições ou do ficheiro de configuração", + "ApiKeyValidationHealthCheckMessage": "Por favor, actualize a sua API Key para ter no minimo {length} caracteres. Pode fazer através das definições ou do ficheiro de configuração", "ApplicationURL": "URL da aplicação", "Genre": "Gêneros", "ThemeHelpText": "Alterar o tema da interface do usuário. O tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por Theme.Park", @@ -420,7 +420,7 @@ "ApplyTagsHelpTextRemove": "Remover: eliminar as etiquetas adicionadas", "DeleteSelectedDownloadClients": "Eliminar cliente de transferências", "DeleteSelectedApplicationsMessageText": "Tem a certeza que quer eliminar as {count} aplicações seleccionadas?", - "DeleteSelectedDownloadClientsMessageText": "Tem a certeza que quer eliminar os {count} clientes de transferência seleccionados?", + "DeleteSelectedDownloadClientsMessageText": "Tem a certeza de que pretende eliminar o(s) cliente(s) de {count} transferência selecionado(s)?", "DeleteSelectedIndexersMessageText": "Tem a certeza que quer eliminar os {count} indexadores seleccionados?", "DownloadClientPriorityHelpText": "Priorizar múltiplos clientes de transferências. Utilizaremos round robin para clientes com a mesma prioridade.", "More": "Mais", @@ -431,10 +431,10 @@ "Season": "Temporada", "Theme": "Tema", "Track": "Rastreio", - "UpdateAvailable": "Nova atualização disponível", + "UpdateAvailableHealthCheckMessage": "Nova atualização disponível", "Label": "Rótulo", - "ConnectionLostReconnect": "O Radarr tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", - "ConnectionLostToBackend": "O Radarr perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLostReconnect": "O {appName} tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", + "ConnectionLostToBackend": "O {appName} perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", "WhatsNew": "O que há de novo?", "RecentChanges": "Mudanças recentes", "minutes": "Minutos", @@ -472,5 +472,36 @@ "EditSelectedDownloadClients": "Editar Clientes de Transferência Selecionados", "Album": "álbum", "Artist": "artista", - "DefaultNameCopiedProfile": "{name} - Copiar" + "DefaultNameCopiedProfile": "{name} - Copiar", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmar nova senha", + "AdvancedSettingsHiddenClickToShow": "Definições avançadas ocultas, clique para mostrar", + "AddCategory": "Adicionar Categoria", + "CountApplicationsSelected": "{count} Coleções Selecionadas", + "IndexerHDBitsSettingsMediums": "Médio", + "Directory": "Diretório", + "UseSsl": "Usar SSL", + "IndexerHDBitsSettingsCodecs": "Codificador", + "CustomFilter": "Filtros personalizados", + "Clone": "Clonar", + "GrabRelease": "Capturar versão", + "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", + "ProxyValidationBadRequest": "Falha ao testar o proxy. Código de estado: {statusCode}", + "Default": "Predefinição", + "Any": "Quaisquer", + "Script": "Script", + "BuiltIn": "Incorporado", + "PublishedDate": "Data de publicação", + "AllSearchResultsHiddenByFilter": "Todos os resultados foram ocultados pelo filtro aplicado", + "BlackholeFolderHelpText": "Pasta em que {appName} guardará o ficheiro {extension}.", + "AptUpdater": "Utilize o apt para instalar a atualização", + "DockerUpdater": "atualize o contentor do Docker para receber a atualização", + "Download": "Transferência", + "ErrorRestoringBackup": "Erro ao restaurar cópia de segurança", + "ExternalUpdater": "O {appName} está definido para usar um mecanismo de atualização externo", + "NoEventsFound": "Nenhum evento encontrado", + "RestartReloadNote": "Nota: o {appName} reiniciará e recarregará automaticamente a IU durante o processo de restauração.", + "UpdateAppDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,", + "InstallLatest": "Instalar o mais recente", + "CurrentlyInstalled": "Atualmente instalado", + "Mixed": "Corrigido" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 3e2129e72..43c1f2932 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -15,17 +15,17 @@ "AddToDownloadClient": "Adicionar lançamento ao cliente de download", "Added": "Adicionado", "AddedToDownloadClient": "Lançamento adicionado ao cliente", - "AddingTag": "Adicionar tag", + "AddingTag": "Adicionar etiqueta", "Age": "Tempo de vida", "Album": "Álbum", "All": "Todos", "AllIndexersHiddenDueToFilter": "Todos os indexadores estão ocultos devido ao filtro aplicado.", "Analytics": "Análises", - "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas da interface Web do {appName} você usa, relatórios de erros, e a versão do sistema operacional e do tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", + "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas da interface Web do {appName} você usa, relatórios de erros, a versão do sistema operacional e do tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", "ApiKey": "Chave da API", - "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {0} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", + "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "AppDataDirectory": "Diretório AppData", - "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na atualização", + "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na Atualização", "AppProfileInUse": "Perfil de aplicativo em uso", "AppProfileSelectHelpText": "Os perfis de aplicativos são usados para controlar as configurações de RSS, Pesquisa automática e Pesquisa interativa ao sincronizar o aplicativo", "AppSettingsSummary": "Aplicativos e configurações para configurar como {appName} interage com seus programas PVR", @@ -38,7 +38,7 @@ "ApplicationUrlHelpText": "A URL externa deste aplicativo, incluindo http(s)://, porta e URL base", "Applications": "Aplicativos", "Apply": "Aplicar", - "ApplyTags": "Aplicar Tags", + "ApplyTags": "Aplicar etiquetas", "Apps": "Aplicativos", "AreYouSureYouWantToDeleteCategory": "Tem certeza de que deseja excluir a categoria mapeada?", "Artist": "Artista", @@ -48,7 +48,7 @@ "AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar o {appName}", "AuthenticationRequired": "Autenticação exigida", "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", - "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação de endereços locais.", + "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, o {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação para endereços locais.", "Author": "Autor", "Automatic": "Automático", "AutomaticSearch": "Pesquisa automática", @@ -57,24 +57,24 @@ "BackupFolderHelpText": "Os caminhos relativos estarão no diretório AppData do {appName}", "BackupIntervalHelpText": "Intervalo entre backups automáticos", "BackupNow": "Fazer backup agora", - "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão limpos automaticamente", + "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão excluídos automaticamente", "Backups": "Backups", "BeforeUpdate": "Antes da atualização", - "BindAddress": "Fixar endereço", + "BindAddress": "Vincular endereço", "BindAddressHelpText": "Endereço IP válido, localhost ou '*' para todas as interfaces", "Book": "Livro", "BookSearch": "Pesquisar Livro", "BookSearchTypes": "Tipos de Pesquisa de Livros", "Branch": "Ramificação", - "BranchUpdate": "Ramificação para atualização do {appName}", + "BranchUpdate": "Ramificação para atualizar o {appName}", "BranchUpdateMechanism": "Ramificação usada pelo mecanismo externo de atualização", "BypassProxyForLocalAddresses": "Ignorar proxy para endereços locais", "Cancel": "Cancelar", - "CancelPendingTask": "Tem certeza de que deseja cancelar essa tarefa pendente?", + "CancelPendingTask": "Tem certeza de que deseja cancelar esta tarefa pendente?", "Categories": "Categorias", "Category": "Categoria", "CertificateValidation": "Validação de certificado", - "CertificateValidationHelpText": "Alterar o quão estrita é a validação da certificação HTTPS", + "CertificateValidationHelpText": "Alterar a rigidez da validação da certificação HTTPS", "ChangeHasNotBeenSavedYet": "A alteração ainda não foi salva", "Clear": "Limpar", "ClearHistory": "Limpar histórico", @@ -93,7 +93,7 @@ "CouldNotConnectSignalR": "Não é possível conectar ao SignalR, a interface não atualizará", "Custom": "Personalizado", "CustomFilters": "Filtros personalizados", - "DBMigration": "Migração de banco de dados", + "DatabaseMigration": "Migração de banco de dados", "Database": "Banco de dados", "Date": "Data", "Dates": "Datas", @@ -101,7 +101,7 @@ "DeleteAppProfile": "Excluir perfil do aplicativo", "DeleteApplication": "Excluir aplicativo", "DeleteApplicationMessageText": "Tem certeza de que deseja excluir o aplicativo '{name}'?", - "DeleteBackup": "Excluir Backup", + "DeleteBackup": "Excluir backup", "DeleteBackupMessageText": "Tem certeza de que deseja excluir o backup '{name}'?", "DeleteClientCategory": "Excluir Categoria de Cliente de Download", "DeleteDownloadClient": "Excluir cliente de download", @@ -110,8 +110,8 @@ "DeleteIndexerProxyMessageText": "Tem certeza de que deseja excluir o proxy do indexador '{name}'?", "DeleteNotification": "Excluir notificação", "DeleteNotificationMessageText": "Tem certeza de que deseja excluir a notificação '{name}'?", - "DeleteTag": "Excluir tag", - "DeleteTagMessageText": "Tem certeza de que deseja excluir a tag '{label}'?", + "DeleteTag": "Excluir etiqueta", + "DeleteTagMessageText": "Tem certeza de que deseja excluir a etiqueta '{label}'?", "Description": "Descrição", "Details": "Detalhes", "DevelopmentSettings": "Configurações de desenvolvimento", @@ -123,10 +123,10 @@ "DownloadClient": "Cliente de download", "DownloadClientCategory": "Categoria de Download do Cliente", "DownloadClientSettings": "Configurações do cliente de download", - "DownloadClientStatusCheckAllClientMessage": "Todos os clientes de download estão indisponíveis devido a falhas", - "DownloadClientStatusCheckSingleClientMessage": "Clientes de download indisponíveis devido a falhas: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "Todos os clientes de download estão indisponíveis devido a falhas", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de download indisponíveis devido a falhas: {downloadClientNames}", "DownloadClients": "Clientes de download", - "DownloadClientsSettingsSummary": "Configuração de clientes de download para integração com a pesquisa da interface do usuário do {appName}", + "DownloadClientsSettingsSummary": "Configuração de clientes de download para integração com a pesquisa da interface do {appName}", "Duration": "Duração", "Edit": "Editar", "EditIndexer": "Editar Indexador", @@ -163,7 +163,7 @@ "Fixed": "Corrigido", "FocusSearchBox": "Selecionar a caixa de pesquisa", "Folder": "Pasta", - "ForMoreInformationOnTheIndividualDownloadClients": "Para saber mais sobre cada cliente de download, clique nos botões de informações.", + "ForMoreInformationOnTheIndividualDownloadClients": "Para saber mais sobre os clientes de download individuais, clique nos botões de informações.", "FullSync": "Sincronização completa", "General": "Geral", "GeneralSettings": "Configurações gerais", @@ -173,8 +173,8 @@ "GrabTitle": "Obter Título", "Grabbed": "Obtido", "Grabs": "Obtenções", - "Health": "Saúde", - "HealthNoIssues": "Nenhum problema com sua configuração", + "Health": "Integridade", + "NoIssuesWithYourConfiguration": "Nenhum problema com sua configuração", "HideAdvanced": "Ocultar opções avançadas", "History": "Histórico", "HistoryCleanup": "Histórico de Limpeza", @@ -198,26 +198,26 @@ "IndexerFlags": "Sinalizadores do indexador", "IndexerHealthCheckNoIndexers": "Não há indexadores habilitados, o {appName} não retornará resultados para a pesquisa", "IndexerInfo": "Info do Indexador", - "IndexerLongTermStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a falhas por mais de 6 horas", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas por mais de 6 horas", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", "IndexerName": "Nome do Indexador", - "IndexerNoDefCheckMessage": "Os indexadores não têm definição e não funcionarão: {0}. Por favor, remova e (ou) adicione novamente ao {appName}", + "IndexerNoDefinitionCheckHealthCheckMessage": "Os indexadores não têm definição e não funcionarão: {indexerNames}. Remova e (ou) adicione novamente ao {appName}.", "IndexerObsoleteCheckMessage": "Os seguintes indexadores são obsoletos ou foram atualizados: {0}. Remova-os e/ou adicione-os novamente ao {appName}", "IndexerPriority": "Prioridade do indexador", - "IndexerPriorityHelpText": "Prioridade do Indexador de 1 (Mais Alta) a 50 (Mais Baixa). Padrão: 25.", + "IndexerPriorityHelpText": "Prioridade do indexador, de 1 (mais alta) a 50 (mais baixa). Padrão: 25.", "IndexerProxies": "Proxies do Indexador", "IndexerProxy": "Proxy do Indexador", - "IndexerProxyStatusCheckAllClientMessage": "Todos os proxies estão indisponíveis devido a falhas", - "IndexerProxyStatusCheckSingleClientMessage": "Proxies indisponíveis devido a falhas: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Todos os proxies estão indisponíveis devido a falhas", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxies indisponíveis devido a falhas: {indexerProxyNames}", "IndexerQuery": "Consulta do indexador", "IndexerRss": "RSS do indexador", "IndexerSettingsSummary": "Defina várias configurações globais do Indexador, incluindo Proxies.", "IndexerSite": "Site do Indexador", - "IndexerStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a falhas", - "IndexerStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a falhas: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", + "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", "IndexerTagsHelpText": "Use tags para especificar Proxies do indexador ou com quais aplicativos o indexador está sincronizado.", - "IndexerVipCheckExpiredClientMessage": "Benefícios VIP do Indexador expiraram: {0}", - "IndexerVipCheckExpiringClientMessage": "Os benefícios VIPS do Indexador expirarão em breve: {0}", + "IndexerVipExpiredHealthCheckMessage": "Benefícios VIP do Indexador expiraram: {indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "Os benefícios VIPS do Indexador expirarão em breve: {indexerNames}", "Indexers": "Indexadores", "Info": "Informações", "InitialFailure": "Falha Inicial", @@ -225,30 +225,30 @@ "InstanceNameHelpText": "Nome da instância na aba e para o nome do aplicativo Syslog", "InteractiveSearch": "Pesquisa interativa", "Interval": "Intervalo", - "KeyboardShortcuts": "Atalhos de teclado", + "KeyboardShortcuts": "Atalhos do teclado", "Label": "Rótulo", "Language": "Idioma", - "LastDuration": "Última Duração", - "LastExecution": "Última Execução", + "LastDuration": "Última duração", + "LastExecution": "Última execução", "LastFailure": "Última Falha", - "LastWriteTime": "Hora da Última Gravação", - "LaunchBrowserHelpText": " Abrir o navegador Web e navegar até a página inicial do {appName} ao iniciar o aplicativo.", + "LastWriteTime": "Hora da última gravação", + "LaunchBrowserHelpText": " Abrir um navegador e navegar até a página inicial do {appName} ao iniciar o aplicativo.", "Level": "Nível", "Link": "Link", - "LogFiles": "Arquivos de registro", - "LogLevel": "Nível de registro", - "LogLevelTraceHelpTextWarning": "O registro em log deve ser habilitado apenas temporariamente", + "LogFiles": "Arquivos de log", + "LogLevel": "Nível de registro em log", + "LogLevelTraceHelpTextWarning": "O registro em log para rastreamento deve ser habilitado apenas temporariamente", "Logging": "Registro em log", "Logs": "Registros", - "MIA": "Desaparecidos", - "MaintenanceRelease": "Versão de manutenção: correções de bugs e outros aprimoramentos. Consulte o Histórico de Commit do Github para obter mais detalhes", + "MIA": "Ausentes", + "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commit do Github para saber mais", "Manual": "Manual", "MappedCategories": "Categorias Mapeadas", "MappedDrivesRunningAsService": "As unidades de rede mapeadas não estão disponíveis quando executadas como um serviço do Windows. Consulte as Perguntas frequentes para saber mais", "MassEditor": "Editor em Massa", "Mechanism": "Mecanismo", "Message": "Mensagem", - "MinimumSeeders": "Mínimo de Seeders", + "MinimumSeeders": "Mínimo de semeadores", "MinimumSeedersHelpText": "Semeadores mínimos exigidos pelo aplicativo para o indexador baixar", "Mode": "Modo", "More": "Mais", @@ -311,9 +311,9 @@ "Proxies": "Proxies", "Proxy": "Proxy", "ProxyBypassFilterHelpText": "Use ',' como separador e '*.' como curinga para subdomínios", - "ProxyCheckBadRequestMessage": "Falha ao testar o proxy. Código de status: {0}", - "ProxyCheckFailedToTestMessage": "Falha ao testar o proxy: {0}", - "ProxyCheckResolveIpMessage": "Falha ao resolver o endereço IP do host de proxy configurado {0}", + "ProxyBadRequestHealthCheckMessage": "Falha ao testar o proxy. Código de status: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Falha ao testar o proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Falha ao resolver o endereço IP do host de proxy configurado {proxyHostName}", "ProxyPasswordHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.", "ProxyType": "Tipo de Proxy", "ProxyUsernameHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.", @@ -324,8 +324,8 @@ "QueryResults": "Resultados da Consulta", "Queue": "Fila", "Queued": "Na fila", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "O RSS não é compatível com este indexador", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "O RSS não é compatível com este indexador", "RawSearchSupported": "Pesquisa Bruta Suportada", "ReadTheWikiForMoreInformation": "Leia o Wiki para obter mais informações", "Reddit": "Reddit", @@ -401,7 +401,7 @@ "Source": "Origem", "StartTypingOrSelectAPathBelow": "Comece a digitar ou selecione um caminho abaixo", "Started": "Iniciado", - "StartupDirectory": "Diretório de inicialização", + "StartupDirectory": "Diretório de Inicialização", "Stats": "Estatísticas", "Status": "Status", "StopSelecting": "Parar Seleção", @@ -414,7 +414,7 @@ "SyncProfile": "Perfil de Sincronização", "SyncProfiles": "Perfis de Sincronização", "System": "Sistema", - "SystemTimeCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido", + "SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido", "TVSearchTypes": "Tipos de Pesquisa de Seriados", "TableOptions": "Opções de Tabela", "TableOptionsColumnsMessage": "Escolha quais colunas são visíveis e em que ordem aparecem", @@ -422,14 +422,14 @@ "TagIsNotUsedAndCanBeDeleted": "A tag não é usada e pode ser excluída", "Tags": "Etiquetas", "TagsHelpText": "Aplica-se a indexadores com pelo menos uma tag correspondente", - "TagsSettingsSummary": "Veja todas as tags e como elas são usadas. Tags não utilizadas podem ser removidas", + "TagsSettingsSummary": "Veja todas as etiquetas e como elas são usadas. Etiquetas não utilizadas podem ser removidas", "Tasks": "Tarefas", "Test": "Testar", "TestAll": "Testar tudo", "TestAllApps": "Testar todos os aplicativos", "TestAllClients": "Testar todos os clientes", "TestAllIndexers": "Testar todos os indexadores", - "TheLatestVersionIsAlreadyInstalled": "A versão mais recente do {appName} já está instalada", + "OnLatestVersion": "A versão mais recente do {appName} já está instalada", "Theme": "Tema", "ThemeHelpText": "Alterar o tema da interface do usuário do aplicativo, o tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por {inspiredBy}.", "Time": "Tempo", @@ -461,10 +461,10 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "Não foi possível adicionar um novo proxy indexador, tente novamente.", "UnableToAddANewNotificationPleaseTryAgain": "Não foi possível adicionar uma nova notificação. Tente novamente.", "UnableToLoadAppProfiles": "Não foi possível carregar os perfis de aplicativos", - "UnableToLoadApplicationList": "Não é possível carregar a lista de aplicativos", - "UnableToLoadBackups": "Não foi possível carregar os backups", + "ApplicationsLoadError": "Não é possível carregar a lista de aplicativos", + "BackupsLoadError": "Não foi possível carregar os backups", "UnableToLoadDevelopmentSettings": "Não foi possível carregar as configurações de desenvolvimento", - "UnableToLoadDownloadClients": "Não foi possível carregar os clientes de download", + "DownloadClientsLoadError": "Não foi possível carregar os clientes de download", "UnableToLoadGeneralSettings": "Não foi possível carregar as configurações gerais", "UnableToLoadHistory": "Não foi possível carregar o histórico", "UnableToLoadIndexerProxies": "Não foi possível carregar proxies do indexador", @@ -475,10 +475,10 @@ "UnsavedChanges": "Alterações Não Salvas", "UnselectAll": "Desmarcar tudo", "UpdateAutomaticallyHelpText": "Baixe e instale atualizações automaticamente. Você ainda poderá instalar a partir do Sistema: Atualizações", - "UpdateAvailable": "Nova atualização está disponível", - "UpdateCheckStartupNotWritableMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' não pode ser gravada pelo usuário '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' está em uma pasta de translocação de aplicativo.", - "UpdateCheckUINotWritableMessage": "Não é possível instalar a atualização porque a pasta de IU '{0}' não pode ser gravada pelo usuário '{1}'.", + "UpdateAvailableHealthCheckMessage": "Nova atualização está disponível: {version}", + "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' não pode ser gravada pelo usuário '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' está em uma pasta de translocação de aplicativo.", + "UpdateUiNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de IU '{uiFolder}' não pode ser gravada pelo usuário '{userName}'.", "UpdateMechanismHelpText": "Usar o atualizador integrado do {appName} ou um script", "UpdateScriptPathHelpText": "Caminho para um script personalizado que usa um pacote de atualização extraído e lida com o restante do processo de atualização", "Updates": "Atualizações", @@ -499,21 +499,21 @@ "Yes": "Sim", "YesCancel": "Sim, Cancelar", "Yesterday": "Ontem", - "ApplyChanges": "Aplicar Mudanças", + "ApplyChanges": "Aplicar mudanças", "ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas", "Implementation": "Implementação", "SelectIndexers": "Pesquisar indexadores", "ApplyTagsHelpTextHowToApplyApplications": "Como aplicar tags ao autor selecionado", - "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar tags aos indexadores selecionados", + "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", "ApplyTagsHelpTextRemove": "Remover: remove as etiquetas inseridas", - "ApplyTagsHelpTextReplace": "Substituir: Substitua as etiquetas pelas etiquetas inseridas (não digite nenhuma etiqueta para limpar todas as etiquetas)", + "ApplyTagsHelpTextReplace": "Substituir: substitui as etiquetas atuais pelas inseridas (deixe em branco para limpar todas as etiquetas)", "CountDownloadClientsSelected": "{count} cliente(s) de download selecionado(s)", "CountIndexersSelected": "{count} indexador(es) selecionado(s)", "DeleteSelectedApplicationsMessageText": "Tem certeza de que deseja excluir {count} aplicativos selecionados?", "DeleteSelectedDownloadClients": "Excluir cliente(s) de download", - "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir {count} cliente(s) de download selecionado(s)?", - "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir {count} indexadores selecionados?", - "DownloadClientPriorityHelpText": "Priorizar vários clientes de download. Usamos um rodízio para clientes com a mesma prioridade.", + "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir o(s) {count} cliente(s) de download selecionado(s)?", + "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir o(s) {count} indexador(es) selecionado(s)?", + "DownloadClientPriorityHelpText": "Priorizar vários clientes de download. Usamos uma distribuição equilibrada para clientes com a mesma prioridade.", "EditSelectedDownloadClients": "Editar clientes de download selecionados", "EditSelectedIndexers": "Editar indexadores selecionados", "ManageDownloadClients": "Gerenciar clientes de download", @@ -531,17 +531,17 @@ "FoundCountReleases": "Encontrou {itemCount} lançamentos", "DeleteSelectedIndexer": "Excluir indexador selecionado", "IndexerCategories": "Categorias do indexador", - "IndexerDownloadClientHelpText": "Especifique qual cliente de download é usado para capturas feitas no {appName} a partir deste indexador", + "IndexerDownloadClientHelpText": "Especifique qual cliente de download é usado para baixar deste indexador no {appName}", "IndexerStatus": "Status do indexador", "ManageApplications": "Gerenciar aplicativos", "NoHistoryFound": "Nenhum histórico encontrado", "PackSeedTime": "Tempo de Semente do Pacote", "PackSeedTimeHelpText": "A hora em que um torrent de pacote (temporada ou discografia) deve ser propagado antes de parar, vazio é o padrão do aplicativo", "QueryType": "Tipo de consulta", - "SeedTime": "Tempo de Semeação", + "SeedTime": "Tempo de semeadura", "SearchAllIndexers": "Pesquisar todos os indexadores", "SearchCountIndexers": "Pesquisar {count} indexador(es)", - "SeedRatio": "Proporção de sementes", + "SeedRatio": "Proporção de semeadura", "SeedRatioHelpText": "A proporção que um torrent deve atingir antes de parar, vazio é o padrão do aplicativo", "SeedTimeHelpText": "O tempo que um torrent deve ser propagado antes de parar, vazio é o padrão do aplicativo", "SelectedCountOfCountReleases": "{selectedCount} de {itemCount} lançamentos selecionados", @@ -554,14 +554,14 @@ "SearchQueries": "Consultas de pesquisa", "minutes": "minutos", "days": "dias", - "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {0}.", + "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {indexerNames}.", "DeleteAppProfileMessageText": "Tem certeza de que deseja excluir o perfil do aplicativo '{name}'?", "AppUpdated": "{appName} atualizado", - "AppUpdatedVersion": "{appName} foi atualizado para a versão `{version}`, para obter as alterações mais recentes, você precisará recarregar {appName}", - "ConnectionLostToBackend": "{appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", + "AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName}", + "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", "RecentChanges": "Mudanças Recentes", "WhatsNew": "O que há de novo?", - "ConnectionLostReconnect": "{appName} tentará se conectar automaticamente ou você pode clicar em recarregar abaixo.", + "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.", "AddApplicationImplementation": "Adicionar Aplicativo - {implementationName}", "AddConnectionImplementation": "Adicionar conexão - {implementationName}", "EditApplicationImplementation": "Editar Aplicativo - {implementationName}", @@ -576,7 +576,7 @@ "EditIndexerImplementation": "Editar indexador - {implementationName}", "EditIndexerProxyImplementation": "Editar Proxy do Indexador - {implementationName}", "NotificationStatusAllClientHealthCheckMessage": "Todas as notificações estão indisponíveis devido a falhas", - "NotificationStatusSingleClientHealthCheckMessage": "Notificações indisponíveis devido a falhas: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notificações indisponíveis devido a falhas: {notificationNames}", "AuthForm": "Formulário (página de login)", "AuthenticationMethod": "Método de autenticação", "AuthenticationMethodHelpTextWarning": "Selecione um método de autenticação válido", @@ -595,11 +595,217 @@ "NoIndexerHistory": "Nenhum histórico encontrado para este indexador", "TotalGrabs": "Total de Capturas", "TotalQueries": "Total de Consultas", - "ApplicationTagsHelpText": "Sincronize indexadores com este aplicativo que não possuem tags ou que possuem 1 ou mais tags correspondentes", + "ApplicationTagsHelpText": "Sincronize indexadores com este aplicativo que possuem uma ou mais etiquetas correspondentes. Se nenhuma etiqueta estiver listada aqui, nenhum indexador será impedido de sincronizar devido às suas etiquetas.", "ApplicationTagsHelpTextWarning": "As tags devem ser usadas com cautela, pois podem ter efeitos indesejados. Um aplicativo com uma tag só será sincronizado com indexadores que tenham a mesma tag.", "IndexerTagsHelpTextWarning": "As tags devem ser usadas com cautela, pois podem ter efeitos indesejados. Um indexador com uma tag sincronizará apenas com aplicativos com a mesma tag.", "NoIndexerCategories": "Nenhuma categoria encontrada para este indexador", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirme a nova senha", "PasswordConfirmation": "Confirmação Da Senha", - "InvalidUILanguage": "Sua UI está definida com um idioma inválido, corrija-a e salve suas configurações" + "InvalidUILanguage": "Sua interface está definida com um idioma inválido, corrija-o e salve suas configurações", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Se devemos usar o layout de conteúdo configurado do qBittorrent, o layout original do torrent ou sempre criar uma subpasta (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsContentLayout": "Layout de conteúdo", + "IndexerId": "ID do Indexador", + "DownloadClientAriaSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Aria2", + "ManageClients": "Gerenciar clientes", + "NoApplicationsFound": "Nenhum aplicativo encontrado", + "NotificationsEmailSettingsUseEncryptionHelpText": "Se preferir usar criptografia se configurado no servidor, usar sempre criptografia via SSL (somente porta 465) ou StartTLS (qualquer outra porta) ou nunca usar criptografia", + "NotificationsEmailSettingsUseEncryption": "Usar Criptografia", + "IndexerHDBitsSettingsPasskeyHelpText": "Chave de Acesso dos Detalhes do Usuário", + "IndexerSettingsPasskey": "Chave de acesso", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Excluir lançamentos SCENE dos resultados", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Pesquisar lançamentos freeleech somente", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Pesquisar lançamentos freeleech somente", + "IndexerBeyondHDSettingsLimitedOnly": "Apenas Limitado", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Pesquisar apenas freeleech (UL limitado)", + "IndexerBeyondHDSettingsRefundOnly": "Só Reembolso", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Pesquisar só reembolso", + "IndexerBeyondHDSettingsRewindOnly": "Só Retroceder", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Pesquisar só retroceder", + "IndexerBeyondHDSettingsRssKeyHelpText": "Chave RSS do Site (Encontrada em Minha Segurança = Chave RSS)", + "IndexerBeyondHDSettingsSearchTypes": "Tipos de Pesquisa", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech", + "IndexerFileListSettingsPasskeyHelpText": "Chave de acesso do site (esta é a string alfanumérica no URL do rastreador mostrado no seu cliente de download)", + "IndexerFileListSettingsUsernameHelpText": "Nome de Usuário do Site", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Deve ter permissões de usuário e torrents", + "IndexerGazelleGamesSettingsSearchGroupNames": "Pesquisar Nomes de Grupos", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Pesquisar lançamentos por nomes de grupos", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsOriginsHelpText": "Se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsUseFilenames": "Usar nomes de arquivos", + "IndexerHDBitsSettingsUsernameHelpText": "Nome de Usuário do Site", + "IndexerHDBitsSettingsMediums": "Meios", + "IndexerHDBitsSettingsOrigins": "Origens", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech", + "IndexerNebulanceSettingsApiKeyHelpText": "Chave de API nas chaves de API das configurações do usuário. A chave deve ter permissões de Lista e Download", + "IndexerNewznabSettingsApiKeyHelpText": "Chave de API do site", + "IndexerNzbIndexSettingsApiKeyHelpText": "Chave de API do site", + "IndexerOrpheusSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Chave de API do site", + "IndexerPassThePopcornSettingsApiUserHelpText": "Essas configurações são encontradas nas configurações de segurança do PassThePopcorn (Editar Perfil > Segurança).", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech", + "IndexerRedactedSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)", + "IndexerSettingsAdditionalParameters": "Parâmetros Adicionais", + "IndexerSettingsApiPath": "Caminho da API", + "IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}", + "IndexerSettingsApiUser": "Usuário da API", + "IndexerSettingsBaseUrl": "URL Base", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "Cookie do site", + "IndexerSettingsFreeleechOnly": "Só Freeleech", + "IndexerSettingsGrabLimit": "Limite de Captura", + "IndexerSettingsGrabLimitHelpText": "O número máximo de capturas conforme especificado pela respectiva unidade que {appName} permitirá ao site", + "IndexerSettingsLimitsUnit": "Unidade de Limites", + "IndexerSettingsLimitsUnitHelpText": "A unidade de tempo para contagem de limites por indexador", + "IndexerSettingsPackSeedTime": "Tempo de Semeação de Pack", + "IndexerSettingsQueryLimit": "Limite de Consultas", + "IndexerSettingsRssKey": "Chave RSS", + "IndexerSettingsSeedRatio": "Proporção de semeação", + "IndexerSettingsSeedTime": "Tempo de semeação", + "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser semeado antes de parar, vazio usa o padrão do cliente de download", + "IndexerSettingsVipExpiration": "Expiração VIP", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Chave de API do site", + "IndexerAlphaRatioSettingsExcludeScene": "Excluir SCENE", + "IndexerBeyondHDSettingsApiKeyHelpText": "Chave de API do site (encontrada em Minha segurança = chave de API)", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Selecione os tipos de lançamentos nos quais você está interessado. Se nenhum for selecionado, todas as opções serão usadas.", + "IndexerHDBitsSettingsCodecsHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostrar apenas lançamentos freeleech", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Marque esta opção se quiser usar nomes de arquivos torrent como títulos de lançamento", + "IndexerIPTorrentsSettingsCookieUserAgent": "Agente de Usuário para Cookies", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Agente de Usuário associado ao cookie usado no Navegador", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Parâmetros adicionais do Newznab", + "IndexerNewznabSettingsVipExpirationHelpText": "Insira a data (aaaa-mm-dd) para expiração do VIP ou em branco, {appName} notificará 1 semana após a expiração do VIP", + "IndexerSettingsAppsMinimumSeeders": "Semeadores mínimos de aplicativos", + "IndexerSettingsAppsMinimumSeedersHelpText": "Semeadores mínimos exigidos pelos aplicativos para o indexador capturar, vazio é o padrão do perfil de sincronização", + "IndexerSettingsBaseUrlHelpText": "Selecione qual URL base {appName} usará para solicitações ao site", + "IndexerSettingsPackSeedTimeIndexerHelpText": "O tempo que um pack de torrent (temporada ou discografia) deve ser semeado antes de parar, vazio é o padrão do aplicativo", + "IndexerSettingsQueryLimitHelpText": "O número máximo de consultas especificadas pela respectiva unidade que {appName} permitirá ao site", + "IndexerSettingsSeedRatioHelpText": "A proporção que um torrent deve atingir antes de parar, vazio usa o padrão do cliente de download. A proporção deve ser de pelo menos 1,0 e seguir as regras dos indexadores", + "DefaultCategory": "Categoria Padrão", + "Destination": "Destino", + "Directory": "Diretório", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL do JSON do Deluge, consulte {url}", + "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionais", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Adiciona um prefixo à API do Flood, como {url}", + "DownloadClientFreeboxSettingsApiUrl": "URL da API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API do Freebox com a versão da API, por exemplo, \"{url}\", o padrão é \"{defaultApiUrl}\"", + "DownloadClientFreeboxSettingsAppId": "ID do aplicativo", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID do aplicativo fornecida ao criar acesso à API do Freebox (ou seja, \"app_id\")", + "DownloadClientFreeboxSettingsAppToken": "Token do aplicativo", + "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é \"{port}\"", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos a versão 16.0 do NzbGet", + "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Priorizar o primeiro e o último", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem sequencial", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixar em ordem sequencial (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usar uma conexão segura. Consulte Opções -> Interface de Usuário da Web -> \"Usar HTTPS ao invés do HTTP\" no qBittorrent.", + "DownloadClientRTorrentSettingsAddStopped": "Adicionar parado", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitar esta opção adicionará os torrents e links magnéticos ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", + "DownloadClientRTorrentSettingsUrlPath": "Caminho do URL", + "DownloadClientSettingsAddPaused": "Adicionar pausado", + "DownloadClientSettingsInitialState": "Estado inicial", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados ao {clientName}", + "DownloadClientSettingsPriorityItemHelpText": "Prioridade de uso ao pegar itens", + "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL do {clientName}, como {url}", + "DownloadClientSettingsUseSslHelpText": "Usar conexão segura ao conectar-se ao {clientName}", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL de chamada remota do {clientName}, por exemplo, {url}. O padrão é \"{defaultUrl}\"", + "TorrentBlackholeSaveMagnetFilesExtension": "Salvar Arquivos Magnet com Extensão", + "TorrentBlackholeTorrentFolder": "Pasta do Torrent", + "UsenetBlackholeNzbFolder": "Pasta Nzb", + "XmlRpcPath": "Caminho RPC XML", + "BlackholeFolderHelpText": "Pasta na qual o {appName} armazenará o arquivo {extension}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", + "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API do Freebox (ou seja, \"app_token\")", + "DownloadClientFreeboxSettingsHostHelpText": "Nome ou endereço IP do host do Freebox, o padrão é \"{url}\" (só funcionará se estiver na mesma rede)", + "DownloadClientPneumaticSettingsNzbFolder": "Pasta Nzb", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisará estar acessível no XBMC", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última partes antes (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que torrents forçados não obedecem às restrições de semeadura", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o ponto de extremidade do XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Categoria de reserva padrão se não existir nenhuma categoria mapeada para um lançamento. Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório [categoria] no diretório de saída.", + "DownloadClientSettingsDefaultCategoryHelpText": "Categoria de reserva padrão se não existir nenhuma categoria mapeada para um lançamento. Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado.", + "DownloadClientSettingsDestinationHelpText": "Especifica manualmente o destino do download, deixe em branco para usar o padrão", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar os downloads, deixe em branco para usar o local padrão do Transmission", + "SecretToken": "Token Secreto", + "TorrentBlackholeSaveMagnetFiles": "Salvar Arquivos Magnets", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensão a ser usada para links magnet, o padrão é '.magnet'", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnet se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", + "UseSsl": "Usar SSL", + "CustomFilter": "Filtro Personalizado", + "LabelIsRequired": "Rótulo é requerido", + "ProwlarrDownloadClientsInAppOnlyAlert": "Os clientes de download destinam-se apenas a pesquisas no aplicativo {appName} e não sincronizam com aplicativos. Não há planos para adicionar tal funcionalidade.", + "ProwlarrDownloadClientsAlert": "Se você pretende fazer pesquisas diretamente em {appName}, você precisa adicionar Clientes de Download. Caso contrário, você não precisa adicioná-los aqui. Para pesquisas em seus aplicativos, os clientes de download configurados são usados.", + "Menu": "Menu", + "Mixed": "Misturado", + "Donate": "Doar", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Sincronizar Lista de Bloqueio de Hashes de Torrents Rejeitados Enquanto Baixando", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Se um torrent for bloqueado por hash, pode não ser rejeitado corretamente durante a Sincronização RSS/Pesquisa em alguns indexadores. Ativar isso permitirá que ele seja rejeitado após obter o torrent, mas antes de enviar ao cliente.", + "ClickToChangeQueryOptions": "Clique para alterar as opções de consulta", + "IndexerMTeamTpSettingsApiKeyHelpText": "Chave API do Site (Encontrada no Painel de Controle do Usuário => Segurança => Laboratório)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Pesquise apenas lançamentos freeleech", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} no Título", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente, prefixe o título da mensagem com {appName} para diferenciar notificações de diferentes aplicativos", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Pesquise apenas lançamentos freeleech", + "ProxyValidationBadRequest": "Falha ao testar o proxy. Código de status: {statusCode}", + "ProxyValidationUnableToConnect": "Não foi possível conectar-se ao proxy: {exceptionMessage}. Verifique o log em torno deste erro para obter detalhes", + "Default": "Padrão", + "ManualGrab": "Baixar manualmente", + "Open": "Abrir", + "OverrideAndAddToDownloadClient": "Substituir e adicionar ao cliente de download", + "OverrideGrabModalTitle": "Substituir e Baixar - {title}", + "PrioritySettings": "Prioridade: {priority}", + "GrabRelease": "Baixar lançamento", + "SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download", + "Any": "Quaisquer", + "Script": "Script", + "BuiltIn": "Embutido", + "InfoUrl": "URL de informações", + "PublishedDate": "Data de Publicação", + "Redirected": "Redirecionar", + "AverageQueries": "Média de Consultas", + "AverageGrabs": "Média de Capturas", + "AllSearchResultsHiddenByFilter": "Todos os resultados da pesquisa são ocultados pelo filtro aplicado.", + "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.", + "LogSizeLimit": "Limite de Tamanho do Registro", + "LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.", + "PreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent", + "IndexerSettingsPreferMagnetUrl": "Preferir URL Magnético", + "IndexerSettingsPreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent", + "PreferMagnetUrl": "Preferir URL Magnético", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Apenas Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Pesquisar somente lançamentos em Golden Popcorn", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech", + "IndexerAvistazSettingsUsernameHelpText": "Nome de Usuário do Site", + "IndexerAvistazSettingsPasswordHelpText": "Senha do Site", + "IndexerAvistazSettingsPidHelpText": "PID da página Minha Conta ou Meu Perfil", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Somente membros com rank e acima podem usar a API neste indexador.", + "RestartReloadNote": "Observação: o {appName} reiniciará automaticamente e recarregará a interface durante o processo de restauração.", + "DockerUpdater": "Atualize o contêiner do Docker para receber a atualização", + "Download": "Baixar", + "ErrorRestoringBackup": "Erro ao restaurar o backup", + "ExternalUpdater": "O {appName} está configurado para usar um mecanismo de atualização externo", + "FailedToFetchUpdates": "Falha ao buscar atualizações", + "LogFilesLocation": "Os arquivos de log estão localizados em: {location}", + "Logout": "Sair", + "NoEventsFound": "Nenhum evento encontrado", + "TheLogLevelDefault": "O nível de registro é padronizado como 'Info' e pode ser alterado em [Configurações Gerais](/settings/general)", + "UpdateAppDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,", + "UpdaterLogFiles": "Arquivos de log do atualizador", + "WouldYouLikeToRestoreBackup": "Gostaria de restaurar o backup '{name}'?", + "AptUpdater": "Usar apt para instalar atualizações", + "Install": "Instalar", + "InstallLatest": "Instalar o mais recente", + "InstallMajorVersionUpdate": "Instalar Atualização", + "InstallMajorVersionUpdateMessage": "Esta atualização instalará uma nova versão principal e pode não ser compatível com o seu sistema. Tem certeza de que deseja instalar esta atualização?", + "InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.", + "FailedToFetchSettings": "Falha ao obter configurações", + "CurrentlyInstalled": "Atualmente instalado", + "PreviouslyInstalled": "Instalado anteriormente", + "DownloadClientUTorrentProviderMessage": "O uTorrent tem um histórico de incluir criptomineradores, malware e anúncios, recomendamos que você escolha outro cliente de download." } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 8b31ccaa2..2385c5ddf 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -19,7 +19,7 @@ "Added": "Adăugat", "Actions": "Acțiuni", "About": "Despre", - "IndexerStatusCheckAllClientMessage": "Niciun indexator nu este disponibil datorită erorilor", + "IndexerStatusAllUnavailableHealthCheckMessage": "Niciun indexator nu este disponibil datorită erorilor", "Indexers": "Indexatori", "Indexer": "Indexator", "Host": "Gazdă", @@ -37,8 +37,8 @@ "EventType": "Tip de eveniment", "Events": "Evenimente", "Edit": "Editează", - "DownloadClientStatusCheckSingleClientMessage": "Clienții de descărcare sunt indisponibili datorită erorilor: {0}", - "DownloadClientStatusCheckAllClientMessage": "Toți clienții de descărcare sunt indisponibili datorită erorilor", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare sunt indisponibili datorită erorilor: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Toți clienții de descărcare sunt indisponibili datorită erorilor", "Peers": "Parteneri", "PageSize": "Mărimea Paginii", "Options": "Opțiuni", @@ -56,8 +56,8 @@ "Language": "Limbă", "KeyboardShortcuts": "Scurtături din tastatură", "Info": "Info", - "IndexerStatusCheckSingleClientMessage": "Indexatoare indisponibile datorită erorilor: {0}", - "HealthNoIssues": "Nicio problemă de configurare", + "IndexerStatusUnavailableHealthCheckMessage": "Indexatoare indisponibile datorită erorilor: {indexerNames}", + "NoIssuesWithYourConfiguration": "Nicio problemă de configurare", "Error": "Eroare", "ConnectionLost": "Conexiune Pierdută", "Component": "Componentă", @@ -66,17 +66,17 @@ "Cancel": "Anulează", "Apply": "Aplică", "Age": "Vechime", - "ProxyCheckResolveIpMessage": "Nu am putut găsi adresa IP pentru Hostul Proxy Configurat {0}", - "ProxyCheckFailedToTestMessage": "Nu am putut testa proxy: {0}", - "ProxyCheckBadRequestMessage": "Testul proxy a eșuat. StatusCode: {0}", + "ProxyResolveIpHealthCheckMessage": "Nu am putut găsi adresa IP pentru Hostul Proxy Configurat {proxyHostName}", + "ProxyFailedToTestHealthCheckMessage": "Nu am putut testa proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Testul proxy a eșuat. StatusCode: {statusCode}", "Proxy": "Proxy", "Protocol": "Protocol", "Warn": "Atenționare", "View": "Vizualizare", "Updates": "Actualizări", - "UpdateCheckUINotWritableMessage": "Nu pot instala actualizarea pentru că dosarul UI '{0}' nu poate fi scris de către utilizatorul '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Nu pot instala actualizarea pentru că folderul de pornire '{0}' este într-un folder de App Translocation.", - "UpdateCheckStartupNotWritableMessage": "Nu pot instala actualizarea pentru că dosarul '{0}' nu poate fi scris de către utilizatorul '{1}'.", + "UpdateUiNotWritableHealthCheckMessage": "Nu pot instala actualizarea pentru că dosarul UI '{uiFolder}' nu poate fi scris de către utilizatorul '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Nu pot instala actualizarea pentru că folderul de pornire '{startupFolder}' este într-un folder de App Translocation.", + "UpdateStartupNotWritableHealthCheckMessage": "Nu pot instala actualizarea pentru că dosarul '{startupFolder}' nu poate fi scris de către utilizatorul '{userName}'.", "UnselectAll": "Deselectează tot", "UISettingsSummary": "Calendar, dată și setări pentru probleme de vedere", "UI": "Interfață Grafica", @@ -90,7 +90,7 @@ "Tags": "Etichete", "TableOptionsColumnsMessage": "Alege ce coloane sunt vizibile și în ce ordine apar", "TableOptions": "Opțiuni tabel", - "SystemTimeCheckMessage": "Ora sistemului este diferită cu mai mult de 1 zi. Operațiunile programate s-ar putea să nu funcționeze corect până când ora nu este corectată", + "SystemTimeHealthCheckMessage": "Ora sistemului este diferită cu mai mult de 1 zi. Operațiunile programate s-ar putea să nu funcționeze corect până când ora nu este corectată", "System": "Sistem", "Style": "Stil", "Status": "Status", @@ -122,7 +122,7 @@ "SettingsLongDateFormat": "Format de dată lungă", "SettingsShortDateFormat": "Format scurt de dată", "SettingsTimeFormat": "Format ora", - "RSSIsNotSupportedWithThisIndexer": "RSS nu este suportat de acest indexator", + "RssIsNotSupportedWithThisIndexer": "RSS nu este suportat de acest indexator", "ShowSearchHelpText": "Afișați butonul de căutare pe hover", "UILanguageHelpText": "Limba pe care {appName} o va folosi pentru interfața de utilizare", "UILanguageHelpTextWarning": "Reîncărcare browser necesară", @@ -157,8 +157,8 @@ "EnableInteractiveSearchHelpText": "Va fi utilizat atunci când este utilizată căutarea interactivă", "ForMoreInformationOnTheIndividualDownloadClients": "Pentru mai multe informații despre clienții individuali de descărcare, faceți clic pe butoanele de informații.", "IllRestartLater": "Voi reporni mai târziu", - "IndexerProxyStatusCheckAllClientMessage": "Niciun indexator nu este disponibil datorită erorilor", - "IndexerProxyStatusCheckSingleClientMessage": "Proxiuri indisponibile datorită erorilor: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Niciun indexator nu este disponibil datorită erorilor", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Proxiuri indisponibile datorită erorilor: {indexerProxyNames}", "Add": "Adaugă", "Custom": "Personalizat", "DeleteBackup": "Ștergeți copia de rezervă", @@ -173,13 +173,13 @@ "Priority": "Prioritate", "Reddit": "Reddit", "Result": "Rezultat", - "RSS": "RSS", + "Rss": "RSS", "SettingsShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute", "TestAllClients": "Testați toți clienții", "Today": "Astăzi", "UnableToAddANewNotificationPleaseTryAgain": "Imposibil de adăugat o nouă notificare, încercați din nou.", - "UnableToLoadBackups": "Imposibil de încărcat copiile de rezervă", - "UnableToLoadDownloadClients": "Nu se pot încărca clienții de descărcare", + "BackupsLoadError": "Imposibil de încărcat copiile de rezervă", + "DownloadClientsLoadError": "Nu se pot încărca clienții de descărcare", "URLBase": "Baza URL", "UrlBaseHelpText": "Pentru suport proxy invers, implicit este gol", "Usenet": "Usenet", @@ -226,9 +226,9 @@ "BackupRetentionHelpText": "Copiile de rezervă automate mai vechi decât perioada de păstrare vor fi curățate automat", "BindAddress": "Adresa de legare", "ChangeHasNotBeenSavedYet": "Modificarea nu a fost încă salvată", - "CloneProfile": "Clonați profil", + "CloneProfile": "Clonează Profil", "NoLeaveIt": "Nu, lasă-l", - "DBMigration": "Migrarea BD", + "DatabaseMigration": "Migrarea BD", "DeleteBackupMessageText": "Sigur doriți să ștergeți copia de siguranță „{0}”?", "DeleteTagMessageText": "Sigur doriți să ștergeți eticheta '{label}'?", "EnableInteractiveSearch": "Activați căutarea interactivă", @@ -239,8 +239,8 @@ "HomePage": "Pagina principală", "Hostname": "Numele gazdei", "IgnoredAddresses": "Adrese ignorate", - "IndexerLongTermStatusCheckAllClientMessage": "Toți indexatorii sunt indisponibili datorită erorilor de mai mult de 6 ore", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexatori indisponibili datorită erorilor de mai mult de 6 ore: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Toți indexatorii sunt indisponibili datorită erorilor de mai mult de 6 ore", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexatori indisponibili datorită erorilor de mai mult de 6 ore: {indexerNames}", "IndexerPriority": "Prioritate indexator", "Mode": "Mod", "MovieIndexScrollTop": "Index film: Derulați sus", @@ -328,8 +328,8 @@ "EnableRssHelpText": "Activați flux RSS pentru indexator", "IndexerHealthCheckNoIndexers": "Niciun indexator nu este activat, {appName} nu va returna rezultate la căutare.", "IndexerProxy": "Proxy indexator", - "IndexerVipCheckExpiredClientMessage": "Beneficiile VIP pentru indexator au expirat: {0}", - "IndexerNoDefCheckMessage": "Indexatorii nu au definiție și nu vor funcționa: {0}. Vă rugăm să-i ștergeți și (sau) să-i adăugați din nou în {appName}", + "IndexerVipExpiredHealthCheckMessage": "Beneficiile VIP pentru indexator au expirat: {indexerNames}", + "IndexerNoDefinitionCheckHealthCheckMessage": "Indexatorii nu au definiție și nu vor funcționa: {indexerNames}. Vă rugăm să-i ștergeți și (sau) să-i adăugați din nou în {appName}", "IndexerRss": "RSS indexator", "EnabledRedirected": "Activat, Redirecționat", "Ended": "Încheiat", @@ -363,7 +363,7 @@ "FullSync": "Sincronizare completă", "IndexerObsoleteCheckMessage": "Indexatorii sunt învechiți sau nu au fost actualizați: {0}. Vă rugăm să-i ștergeți și (sau) să-i adăugați din nou în {appName}", "IndexerProxies": "Proxiuri indexatoare", - "IndexerVipCheckExpiringClientMessage": "Beneficiile VIP pentru indexator expiră în curând: {0}", + "IndexerVipExpiringHealthCheckMessage": "Beneficiile VIP pentru indexator expiră în curând: {indexerNames}", "ApplicationLongTermStatusCheckAllClientMessage": "Toate aplicațiile sunt indisponibile din cauza unor eșecuri pentru mai mult de 6 ore", "ApplicationLongTermStatusCheckSingleClientMessage": "Aplicațiile indisponibile din cauza unor eșecuri pentru mai mult de 6 ore: {0}", "LastDuration": "Ultima durată", @@ -374,7 +374,7 @@ "NextExecution": "Următoarea execuție", "Remove": "Elimina", "Replace": "A inlocui", - "TheLatestVersionIsAlreadyInstalled": "Cea mai recentă versiune a {appName} este deja instalată", + "OnLatestVersion": "Cea mai recentă versiune a {appName} este deja instalată", "AddApplication": "Adaugă", "AddCustomFilter": "Adaugă filtru personalizat", "Track": "Urmă", @@ -438,13 +438,13 @@ "RecentChanges": "Schimbări recente", "WhatsNew": "Ce mai e nou?", "DeleteAppProfileMessageText": "Sigur doriți să ștergeți profilul de aplicație '{0}'?", - "NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {notificationNames}", "AuthBasic": "Basic (fereastră pop-up browser)", "AuthForm": "Formulare (Pagina de autentificare)", "DisabledForLocalAddresses": "Dezactivat pentru adresele locale", "None": "Nici unul", "ResetAPIKeyMessageText": "Sigur doriți să vă resetați cheia API?", - "AddApplicationImplementation": "Adăugați aplicație - {implementareName}", + "AddApplicationImplementation": "Adăugați aplicație - {implementationName}", "AddCategory": "Adăugați categorie", "AddConnection": "Adăugați conexiune", "AddConnectionImplementation": "Adăugați conexiune - {implementationName}", @@ -462,5 +462,42 @@ "EditIndexerImplementation": "Adăugați Indexator - {implementationName}", "EditIndexerProxyImplementation": "Adăugați proxy indexator - {implementationName}", "EditApplicationImplementation": "Adăugați aplicație - {implementareName}", - "RestartProwlarr": "Reporniți {appName}" + "RestartProwlarr": "Reporniți {appName}", + "AddDownloadClientToProwlarr": "Adăugarea unui client de descărcare permite {appName} să trimită versiuni direct din interfața utilizatorului în timp ce se efectuează o căutare manuală", + "ActiveApps": "Aplicații active", + "IndexerHDBitsSettingsMediums": "Mediu", + "IndexerSettingsVipExpiration": "Expirare VIP", + "Directory": "Director", + "CustomFilter": "Filtru personalizat", + "SelectDownloadClientModalTitle": "{modalTitle} - Selectați clientul de descărcare", + "ProxyValidationBadRequest": "Testul proxy a eșuat. StatusCode: {statusCode}", + "GrabRelease": "Grab Release", + "BuiltIn": "Incorporat", + "Script": "Script", + "InfoUrl": "URL informații", + "PublishedDate": "Data publicării", + "AllSearchResultsHiddenByFilter": "Toate rezultatele sunt ascunse de filtrul aplicat", + "UpdateAvailableHealthCheckMessage": "O nouă versiune este disponibilă: {version}", + "Download": "Descarca", + "ErrorRestoringBackup": "Eroare la restaurarea copiei de rezervă", + "UpdateAppDirectlyLoadError": "Imposibil de actualizat direct {appName},", + "DockerUpdater": "Actualizați containerul Docker pentru a primi actualizarea", + "ExternalUpdater": "{appName} este configurat pentru a utiliza un mecanism de actualizare extern", + "NoEventsFound": "Nu s-au găsit evenimente", + "RestartReloadNote": "Notă: {appName} va reporni și reîncărca automat interfața de utilizare în timpul procesului de restaurare.", + "AptUpdater": "Utilizați apt pentru a instala actualizarea", + "InstallLatest": "Instalați cel mai recent", + "Clone": "Clonează", + "Stats": "Status", + "CurrentlyInstalled": "În prezent instalat", + "Mixed": "Fix", + "Season": "Motiv", + "ActiveIndexers": "Indexatorii activi", + "Any": "Oricare", + "AdvancedSettingsShownClickToHide": "Setări avansate afișate, click pentru a le ascunde", + "AdvancedSettingsHiddenClickToShow": "Setări avansate ascunse, click pentru afișare", + "ApiKeyValidationHealthCheckMessage": "Te rugăm să actualizezi cheia API astfel încât să aibă cel puțin {length} caractere. Poți face acest lucru din setări sau din fișierul de configurare", + "AddToDownloadClient": "Adaugă versiunea în clientul de descărcare", + "AddSyncProfile": "Adaugă profil de sincronizare", + "AddedToDownloadClient": "Versiune adăugată în client" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index a8003a615..f31829160 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -4,67 +4,67 @@ "ClientPriority": "Приоритет клиента", "Backups": "Резервные копии", "BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены", - "BackupNow": "Сделать резервную копию", + "BackupNow": "Создать резервную копию", "BackupIntervalHelpText": "Периодичность автоматического резервного копирования", - "Backup": "Резервная копия", + "Backup": "Резервное копирование", "AutomaticSearch": "Автоматический поиск", - "Authentication": "Аутентификация", - "ApplyTags": "Применить тэги", + "Authentication": "Авторизация", + "ApplyTags": "Применить теги", "Apply": "Применить", - "AppDataLocationHealthCheckMessage": "Обновление будет не возможно, во избежание удаления данных программы во время обновления", + "AppDataLocationHealthCheckMessage": "Обновление не позволит сохранить AppData при обновлении", "ApiKey": "API ключ", "Analytics": "Аналитика", - "AddIndexer": "Добавить индексер", + "AddIndexer": "Добавить индексатор", "Added": "Добавлено", "Actions": "Действия", - "About": "Об", + "About": "О программе", "DeleteBackupMessageText": "Вы уверены, что хотите удалить резервную копию '{name}'?", "DeleteBackup": "Удалить резервную копию", "Delete": "Удалить", "NoLinks": "Нет ссылок", "Refresh": "Обновить", "RefreshMovie": "Обновить фильм", - "RSS": "RSS", - "SendAnonymousUsageData": "Отправить анонимные данные об использовании", + "Rss": "RSS", + "SendAnonymousUsageData": "Отправка анонимных данных об использовании", "UnableToLoadHistory": "Не удалось загрузить историю", - "MoreInfo": "Ещё инфо", - "MovieIndexScrollTop": "Индекс фильма: промотать вверх", + "MoreInfo": "Больше информации", + "MovieIndexScrollTop": "Индекс фильма: Прокрутить вверх", "Size": "Размер", - "OpenBrowserOnStart": "Открывать браузер при запуске", + "OpenBrowserOnStart": "Открыть браузер при запуске", "OpenThisModal": "Открыть это модальное окно", - "Options": "Опции", + "Options": "Параметры", "PackageVersion": "Версия пакета", "PageSize": "Размер страницы", - "PageSizeHelpText": "Количество показываемое на каждой страницы", + "PageSizeHelpText": "Количество элементов, отображаемых на каждой странице", "Password": "Пароль", "Peers": "Пиры", - "PendingChangesDiscardChanges": "Не применять изменения и выйти", - "PendingChangesStayReview": "Оставайтесь и просмотрите изменения", + "PendingChangesDiscardChanges": "Отменить изменения и выйти", + "PendingChangesStayReview": "Остаться и просмотреть изменения", "Port": "Порт", "PortNumber": "Номер порта", "LogFiles": "Файлы журнала", "Details": "Подробности", - "DownloadClients": "Клиенты для скачивания", + "DownloadClients": "Клиенты загрузки", "Filename": "Имя файла", "Files": "Файлы", "Filter": "Фильтр", - "IndexerStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок", - "LogLevel": "Уровень журнала", - "LogLevelTraceHelpTextWarning": "Отслеживание журнала желательно включать только на короткое время", + "IndexerStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок", + "LogLevel": "Уровень журналирования", + "LogLevelTraceHelpTextWarning": "Включение трассировки журнала должно быть временным", "Logs": "Журналы", "Manual": "Ручной", "Mechanism": "Механизм", "NoBackupsAreAvailable": "Нет резервных копий", "DeleteDownloadClientMessageText": "Вы уверены, что хотите удалить клиент загрузки '{name}'?", - "Edit": "Редактирование", + "Edit": "Редактировать", "Enable": "Включить", "Indexer": "Индексатор", "SettingsShortDateFormat": "Короткий формат даты", "SettingsShowRelativeDates": "Показать относительные даты", - "SettingsShowRelativeDatesHelpText": "Показывать относительные (сегодня / вчера / и т. д.) или абсолютные даты", + "SettingsShowRelativeDatesHelpText": "Показывать относительные (Сегодня/Вчера/и т.п.) или абсолютные даты", "Docker": "Docker", "FocusSearchBox": "Поле поиска в фокусе", - "Reset": "Сбросить", + "Reset": "Сброс", "Add": "Добавить", "EnableSSL": "Включить SSL", "IndexerFlags": "Флаги индексатора", @@ -73,36 +73,36 @@ "Usenet": "Usenet", "AcceptConfirmationModal": "Окно подтверждения", "Today": "Сегодня", - "DeleteIndexerProxyMessageText": "Вы уверены, что хотите удалить тэг '{0}'?", - "ExistingTag": "Существующий тэг", + "DeleteIndexerProxyMessageText": "Вы уверены, что хотите удалить прокси индексатора '{name}'?", + "ExistingTag": "Существующий тег", "Failed": "Неудачно", - "IndexerProxyStatusCheckSingleClientMessage": "Индексаторы недоступны из-за ошибок: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Прокси индексатора недоступны из-за ошибок: {indexerProxyNames}", "Indexers": "Индексаторы", "InteractiveSearch": "Интерактивный поиск", "Interval": "Интервал", "KeyboardShortcuts": "Горячие клавиши", "Language": "Язык", "LastWriteTime": "Последнее время записи", - "LaunchBrowserHelpText": " Открывать браузер и переходить на страницу {appName} при запуске программы.", + "LaunchBrowserHelpText": " Открыть браузер и перейти на страницу {appName} при запуске приложения.", "Level": "Уровень", - "Ok": "Ok", - "AddDownloadClient": "Добавить программу для скачивания", - "UpdateMechanismHelpText": "Используйте встроенную в {appName} функцию обновления или скрипт", - "IndexerStatusCheckSingleClientMessage": "Индексаторы недоступны из-за ошибок: {0}", - "NoTagsHaveBeenAddedYet": "Теги еще не добавлены", - "UnableToLoadTags": "Невозможно загрузить теги", - "AnalyticsEnabledHelpText": "Отправлять в {appName} информацию о использовании и ошибках. Анонимная статистика включает в себя информацию о браузере, какие страницы загружены, сообщения об ошибках, а так же операционной системе. Мы используем эту информацию для выявления ошибок, а так же для разработки нового функционала.", - "AuthenticationMethodHelpText": "Необходим логин и пароль для доступа в {appName}", + "Ok": "Ок", + "AddDownloadClient": "Добавить клиент загрузки", + "UpdateMechanismHelpText": "Использовать встроенный инструмент обновления {appName} или скрипт", + "IndexerStatusUnavailableHealthCheckMessage": "Индексаторы недоступны из-за ошибок: {indexerNames}", + "NoTagsHaveBeenAddedYet": "Теги ещё не добавлены", + "UnableToLoadTags": "Не удалось загрузить теги", + "AnalyticsEnabledHelpText": "Отправлять анонимную информацию об использовании и ошибках на серверы {appName}. Анонимная статистика включает в себя информацию о вашем браузере, какие страницы веб-интерфейса {appName} вы используете, отчёты об ошибках, а также версию операционной системы и среды выполнения. Мы будем использовать эту информацию для разработки нового функционала и исправления ошибок.", + "AuthenticationMethodHelpText": "Необходимо ввести имя пользователя и пароль для доступа к {appName}", "BackupFolderHelpText": "Относительные пути будут в каталоге AppData {appName}", "BeforeUpdate": "До обновления", "BindAddress": "Привязать адрес", "BindAddressHelpText": "Действительный IP-адрес, локальный адрес или '*' для всех интерфейсов", - "Branch": "Ветка", + "Branch": "Ветвь", "BranchUpdate": "Ветвь для обновления {appName}", "BranchUpdateMechanism": "Ветвь, используемая внешним механизмом обновления", "BypassProxyForLocalAddresses": "Обход прокси для локальных адресов", - "Cancel": "Отменить", - "CancelPendingTask": "Вы уверены, что хотите убрать данную задачу из очереди?", + "Cancel": "Отмена", + "CancelPendingTask": "Вы уверены, что хотите отменить эту ожидающую задачу?", "CertificateValidation": "Проверка сертификата", "CertificateValidationHelpText": "Изменить строгое подтверждение сертификации НТТР", "ChangeHasNotBeenSavedYet": "Изменения ещё не вступили в силу", @@ -113,79 +113,79 @@ "ConnectionLost": "Соединение прервано", "Connections": "Соединения", "ConnectSettings": "Настройки соединения", - "CouldNotConnectSignalR": "Не возможно подключиться к SignalR, интерфейс не будет обновляться", + "CouldNotConnectSignalR": "Не удалось подключиться к SignalR, интерфейс не будет обновляться", "Custom": "Настраиваемый", "CustomFilters": "Настраиваемые фильтры", "Date": "Дата", "Dates": "Даты", - "DBMigration": "Перенос БД", + "DatabaseMigration": "Миграция базы данных", "DeleteNotification": "Удалить уведомление", "DeleteNotificationMessageText": "Вы уверены, что хотите удалить уведомление '{name}'?", - "DeleteTag": "Удалить тэг", - "DeleteTagMessageText": "Вы уверены, что хотите удалить тэг '{label}'?", + "DeleteTag": "Удалить тег", + "DeleteTagMessageText": "Вы уверены, что хотите удалить тег '{label}'?", "Disabled": "Выключено", "Discord": "Discord", "Donations": "Пожертвования", - "DownloadClient": "Загрузчик", - "DownloadClientSettings": "Настройки клиента скачиваний", - "DownloadClientStatusCheckAllClientMessage": "Все клиенты для скачивания недоступны из-за ошибок", - "DownloadClientStatusCheckSingleClientMessage": "Клиенты для скачивания недоступны из-за ошибок: {0}", + "DownloadClient": "Клиент загрузки", + "DownloadClientSettings": "Настройки клиента загрузки", + "DownloadClientStatusAllClientHealthCheckMessage": "Все клиенты загрузок недоступны из-за ошибок", + "DownloadClientStatusSingleClientHealthCheckMessage": "Клиенты загрузок недоступны из-за ошибок: {downloadClientNames}", "EnableAutomaticSearch": "Включить автоматический поиск", - "EnableAutomaticSearchHelpText": "Будет использовано для автоматических поисков через интерфейс или {appName}", + "EnableAutomaticSearchHelpText": "Будет использовано при автоматическом поиске через интерфейс или {appName}", "Enabled": "Включено", "EnableInteractiveSearch": "Включить интерактивный поиск", "EnableRss": "Включить RSS", "Fixed": "Исправлено", - "Folder": "Папка", - "ForMoreInformationOnTheIndividualDownloadClients": "Для дополнительной информации пл программам скачивания нажмите эту кнопку.", - "General": "Основное", - "GeneralSettings": "Основные настройки", - "GeneralSettingsSummary": "Порт, SSL, логин/пароль, прокси, аналитика и обновления", + "Folder": "Каталог", + "ForMoreInformationOnTheIndividualDownloadClients": "Для получения дополнительной информации о каждом клиенте загрузки нажмите на кнопки информации.", + "General": "Общие", + "GeneralSettings": "Общие настройки", + "GeneralSettingsSummary": "Порт, SSL, имя пользователя/пароль, прокси-сервер, аналитика и обновления", "Grabbed": "Захвачено", - "Grabs": "Захватить", - "Health": "Здоровье", - "HealthNoIssues": "С вашей конфигурацией нет проблем", + "Grabs": "Захваты", + "Health": "Состояние", + "NoIssuesWithYourConfiguration": "С вашей конфигурацией нет проблем", "HideAdvanced": "Скрыть расширенные", "History": "История", "HomePage": "Домашняя страница", "Host": "Хост", "Hostname": "Имя хоста", - "IgnoredAddresses": "Проигнорированные адреса", + "IgnoredAddresses": "Игнорируемые адреса", "IllRestartLater": "Перезапущу позднее", - "IncludeHealthWarningsHelpText": "Включая предупреждения о здоровье", - "IndexerLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов", - "IndexerLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {0}", - "IndexerPriority": "Приоритет индексаторов", - "IndexerPriorityHelpText": "Приоритет индексаторов от 1 (наивысший) до 50 (низший). По-умолчанию: 25.", - "IndexerProxyStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок", + "IncludeHealthWarningsHelpText": "Включая предупреждения о состоянии", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок более 6 часов", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок более 6 часов: {indexerNames}", + "IndexerPriority": "Приоритет индексатора", + "IndexerPriorityHelpText": "Приоритет индексатора от 1 (Высший) до 50 (Низший). По умолчанию 25.", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Все прокси индексатора недоступны из-за ошибок", "Info": "Информация", "Logging": "Журналирование", "Message": "Сообщение", "MIA": "MIA", "Mode": "Режим", - "MovieIndexScrollBottom": "Индекс фильма: промотать в низ", + "MovieIndexScrollBottom": "Индекс фильма: Прокрутить вниз", "Name": "Имя", "New": "Новый", "NoChanges": "Нет изменений", "NoLeaveIt": "Нет, оставить", - "NoLogFiles": "Нет файлов журнала", - "NoUpdatesAreAvailable": "Нет обновлений", - "OAuthPopupMessage": "Ваш браузер блокирует всплывающие окна", - "OnHealthIssueHelpText": "По вопросам здоровья", + "NoLogFiles": "Файлов журнала нет", + "NoUpdatesAreAvailable": "Обновления отсутствуют", + "OAuthPopupMessage": "Всплывающие окна блокируются вашим браузером", + "OnHealthIssueHelpText": "При проблемах с состоянием", "PendingChangesMessage": "У вас есть несохраненные изменения. Вы уверены, что хотите покинуть эту страницу?", - "Presets": "Предустановки", + "Presets": "Пресеты", "Protocol": "Протокол", "Proxy": "Прокси", - "ProxyBypassFilterHelpText": "Используйте ',' в качестве разделителя и '*.' как подстановочный знак для поддоменов", - "ProxyCheckBadRequestMessage": "Не удалось проверить прокси. Код: {0}", - "ProxyCheckFailedToTestMessage": "Не удалось проверить прокси: {0}", - "ProxyCheckResolveIpMessage": "Не удалось преобразовать IP-адрес для настроенного прокси-хоста {0}", - "ProxyPasswordHelpText": "Нужно ввести имя пользователя и пароль только если они необходимы. В противном случае оставьте их пустыми.", + "ProxyBypassFilterHelpText": "Используйте ',' как разделитель и '*.' как подстановочный знак для поддоменов", + "ProxyBadRequestHealthCheckMessage": "Не удалось протестировать прокси. Код состояния: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Не удалось протестировать прокси: {url}", + "ProxyResolveIpHealthCheckMessage": "Не удалось определить IP-адрес настроенного прокси-хоста {proxyHostName}", + "ProxyPasswordHelpText": "Вы должны указать имя пользователя и пароль только если они необходимы. В противном случае оставьте эти поля пустыми.", "ProxyType": "Тип прокси", - "ProxyUsernameHelpText": "Нужно ввести имя пользователя и пароль только если они необходимы. В противном случае оставьте их пустыми.", + "ProxyUsernameHelpText": "Вы должны указать имя пользователя и пароль только если они необходимы. В противном случае оставьте эти поля пустыми.", "Queue": "Очередь", "ReadTheWikiForMoreInformation": "Прочтите Wiki для получения дополнительной информации", - "ReleaseBranchCheckOfficialBranchMessage": "Ветка {0} не является допустимой веткой выпуска {appName}, вы не будете получать обновления", + "ReleaseBranchCheckOfficialBranchMessage": "Ветвь {0} не является действительной релизной ветвью {appName}, вы не будете получать обновления", "ReleaseStatus": "Статус релиза", "Reload": "Перезагрузить", "RemovedFromTaskQueue": "Удалено из очереди задач", @@ -194,12 +194,12 @@ "ResetAPIKey": "Сбросить API ключ", "Restart": "Перезапустить", "RestartNow": "Перезапустить сейчас", - "RestartRequiredHelpTextWarning": "Для вступления в силу требуется перезапуск", + "RestartRequiredHelpTextWarning": "Для применения изменений, требуется перезапуск", "Restore": "Восстановить", - "RestoreBackup": "Восстановить из резервной копии", + "RestoreBackup": "Восстановить резервную копию", "Result": "Результат", "Retention": "Удержание", - "RSSIsNotSupportedWithThisIndexer": "RSS не поддерживается этим индексатором", + "RssIsNotSupportedWithThisIndexer": "RSS не поддерживается этим индексатором", "Save": "Сохранить", "SaveChanges": "Сохранить изменения", "SaveSettings": "Сохранить настройки", @@ -207,217 +207,605 @@ "ScriptPath": "Путь к скрипту", "Search": "Поиск", "Security": "Безопасность", - "Seeders": "Сиды", + "Seeders": "Сидеры", "SelectAll": "Выбрать все", "SetTags": "Установить теги", "Settings": "Настройки", - "SettingsEnableColorImpairedMode": "Версия для слабовидящих", - "SettingsEnableColorImpairedModeHelpText": "Стиль изменён чтобы слабовидящие лучше различали цвета", - "SettingsLongDateFormat": "Длинный формат даты", + "SettingsEnableColorImpairedMode": "Включить режим для людей с ограниченным цветовым восприятием", + "SettingsEnableColorImpairedModeHelpText": "Изменённый стиль позволяет пользователям с нарушениями цветового восприятия лучше различать цветовую информацию", + "SettingsLongDateFormat": "Расширенный формат даты", "SettingsTimeFormat": "Формат времени", "ShowAdvanced": "Показать расширенные", "ShowSearch": "Показать поиск", - "ShowSearchHelpText": "Показать копку поиска по наведению", + "ShowSearchHelpText": "Показать копку поиска при наведении", "Shutdown": "Выключить", "Sort": "Сортировка", - "Source": "Источник", - "SSLCertPassword": "Пароль SSL сертификата", - "SSLCertPasswordHelpText": "Пароль pfx файла", - "SSLCertPath": "Путь SSL сертификата", - "SSLCertPathHelpText": "Путь к pfx файлу", + "Source": "Исходный код", + "SSLCertPassword": "Пароль сертификата SSL", + "SSLCertPasswordHelpText": "Пароль файла pfx", + "SSLCertPath": "Путь к сертификату SSL", + "SSLCertPathHelpText": "Путь к файлу pfx", "StartTypingOrSelectAPathBelow": "Начните вводить или выберите путь ниже", - "StartupDirectory": "Каталог автозагрузки", + "StartupDirectory": "Каталог автозапуска", "Status": "Статус", "Style": "Стиль", "SuggestTranslationChange": "Предложить изменение перевода", - "SystemTimeCheckMessage": "Расхождение системного времени более чем на 1 день. Запланированные задачи могут работать некорректно, пока не будет исправлено время", - "TableOptionsColumnsMessage": "Выберите, какие столбцы отображаются и в каком порядке", - "TagIsNotUsedAndCanBeDeleted": "Тег не используется и может быть удален", - "TagsHelpText": "Применимо к фильмам с хотя бы одним подходящим тегом", + "SystemTimeHealthCheckMessage": "Системное время отклонилось более чем на 1 день. Запланированные задания могут не работать правильно, пока время не будет исправлено", + "TableOptionsColumnsMessage": "Выберите видимые столбцы и порядок их отображения", + "TagIsNotUsedAndCanBeDeleted": "Тег не используется и может быть удалён", + "TagsHelpText": "Применяется к индексаторам, имеющим хотя бы один совпадающий тег", "TagsSettingsSummary": "Посмотрите все теги и способы их использования. Неиспользуемые теги можно удалить", "Tasks": "Задачи", "Test": "Тест", "TestAll": "Тестировать все", - "TestAllClients": "Тестировать всех клиентов", + "TestAllClients": "Тестировать все клиенты", "Time": "Время", "Title": "Название", "Tomorrow": "Завтра", - "Torrent": "Торренты", + "Torrent": "Торрент", "Torrents": "Торренты", "Type": "Тип", "UI": "Пользовательский интерфейс", "UILanguage": "Язык пользовательского интерфейса", "UILanguageHelpTextWarning": "Требуется перезагрузка браузера", "UISettings": "Настройки пользовательского интерфейса", - "UnableToAddANewApplicationPleaseTryAgain": "Невозможно добавить новое уведомление, попробуйте еще раз.", - "UnableToAddANewAppProfilePleaseTryAgain": "Не удалось добавить новый профиль качества. Повторите попытку.", - "UnableToAddANewDownloadClientPleaseTryAgain": "Не удалось добавить новый клиент загрузки, попробуйте еще раз.", - "UnableToAddANewIndexerPleaseTryAgain": "Не удалось добавить новый индексатор, повторите попытку.", - "UnableToAddANewIndexerProxyPleaseTryAgain": "Не удалось добавить новый индексатор, повторите попытку.", - "UnableToAddANewNotificationPleaseTryAgain": "Невозможно добавить новое уведомление, попробуйте еще раз.", - "UnableToLoadBackups": "Невозможно загрузить резервные копии", - "UnableToLoadDownloadClients": "Невозможно загрузить загрузчики", - "UnableToLoadGeneralSettings": "Невозможно загрузить общие настройки", - "UnableToLoadNotifications": "Невозможно загрузить уведомления", - "UnableToLoadUISettings": "Не удалось загрузить настройки пользовательского интерфейса", + "UnableToAddANewApplicationPleaseTryAgain": "Не удалось добавить новое приложение, попробуйте ещё раз.", + "UnableToAddANewAppProfilePleaseTryAgain": "Не удалось добавить новый профиль приложения, попробуйте ещё раз.", + "UnableToAddANewDownloadClientPleaseTryAgain": "Не удалось добавить новый клиент загрузки, попробуйте ещё раз.", + "UnableToAddANewIndexerPleaseTryAgain": "Не удалось добавить новый индексатор, попробуйте ещё раз.", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Не удалось добавить новый прокси индексатора, попробуйте ещё раз.", + "UnableToAddANewNotificationPleaseTryAgain": "Не удалось добавить новое уведомление, попробуйте ещё раз.", + "BackupsLoadError": "Не удалось загрузить резервные копии", + "DownloadClientsLoadError": "Не удалось загрузить клиенты загрузки", + "UnableToLoadGeneralSettings": "Не удалось загрузить общие настройки", + "UnableToLoadNotifications": "Не удалось загрузить уведомления", + "UnableToLoadUISettings": "Не удалось загрузить настройки интерфейса", "UnsavedChanges": "Несохраненные изменения", - "UnselectAll": "Снять все выделения", - "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления. Вы так же можете установить в Система: Обновления", - "UpdateCheckStartupNotWritableMessage": "Невозможно установить обновление так как загрузочная папка '{0}' недоступна для записи для пользователя '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Не удается установить обновление, поскольку папка автозагрузки \"{0}\" находится в папке перемещения приложений.", - "UpdateCheckUINotWritableMessage": "Невозможно установить обновление так как UI папка '{0}' недоступна для записи для пользователя '{1}'.", - "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который обрабатывает остатки после процесса обновления", + "UnselectAll": "Снять выделение со всех", + "UpdateAutomaticallyHelpText": "Автоматически загружать и устанавливать обновления Вы по-прежнему сможете выполнить установку из раздела Система: Обновления", + "UpdateStartupNotWritableHealthCheckMessage": "Невозможно установить обновление, так как каталог автозагрузки '{startupFolder}' недоступен для записи для пользователя '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Невозможно установить обновление, так как каталог автозагрузки '{startupFolder}' находится в каталоге перемещения приложений.", + "UpdateUiNotWritableHealthCheckMessage": "Невозможно установить обновление, так как каталог интерфейса '{uiFolder}' недоступен для записи для пользователя '{userName}'.", + "UpdateScriptPathHelpText": "Путь к пользовательскому скрипту, который извлекает пакет обновления и обрабатывает оставшуюся часть процесса обновления", "Uptime": "Время работы", - "URLBase": "Базовый URL", - "UrlBaseHelpText": "Для поддержки обратного прокси, по умолчанию пусто", + "URLBase": "Базовый URL-адрес", + "UrlBaseHelpText": "Для поддержки обратного прокси, значение по умолчанию - пустая строка", "UseProxy": "Использовать прокси", - "Username": "Пользователь", + "Username": "Имя пользователя", "Version": "Версия", "View": "Просмотр", - "AddingTag": "Добавить ярлык", - "SSLPort": "SSL порт", - "UILanguageHelpText": "Язык, который {appName} будет использовать для пользовательского интерфейса", - "EnableSslHelpText": " Требуется перезапуск от администратора", - "ErrorLoadingContents": "Ошибка при загрузке содержимого", + "AddingTag": "Добавление тега", + "SSLPort": "Порт SSL", + "UILanguageHelpText": "Язык, используемый {appName} для интерфейса пользователя", + "EnableSslHelpText": " Для применения изменений требуется перезапуск с правами администратора", + "ErrorLoadingContents": "Ошибка при загрузке контента", "Events": "События", "EventType": "Тип события", "Exception": "Исключение", - "FeatureRequests": "Будущие запросы", - "Warn": "Предупреждать", + "FeatureRequests": "Запросы функций", + "Warn": "Предупреждение", "Wiki": "Wiki", - "YesCancel": "Да, отменить", + "YesCancel": "Да, Отмена", "Yesterday": "Вчера", "NotificationTriggers": "Триггеры уведомления", - "ApplicationStatusCheckAllClientMessage": "Все листы недоступны из-за ошибок", + "ApplicationStatusCheckAllClientMessage": "Все приложения недоступны из-за ошибок", "Automatic": "Автоматически", - "DeleteApplicationMessageText": "Вы уверены, что хотите удалить уведомление '{0}'?", - "DeleteDownloadClient": "Удалить программу для скачивания", - "EnableInteractiveSearchHelpText": "Будет использовано при автоматических поисках", + "DeleteApplicationMessageText": "Вы уверены, что хотите удалить приложение '{name}'?", + "DeleteDownloadClient": "Удалить клиент загрузки", + "EnableInteractiveSearchHelpText": "Будет использовано при интерактивном поиске", "Error": "Ошибка", "NoChange": "Нет изменений", "Age": "Возраст", "All": "Все", - "AllIndexersHiddenDueToFilter": "Все фильмы спрятаны в соответствии с фильтром.", - "AppDataDirectory": "AppData директория", + "AllIndexersHiddenDueToFilter": "Все индексаторы скрыты применённым фильтром.", + "AppDataDirectory": "Каталог AppData", "Reddit": "Reddit", "System": "Система", - "TableOptions": "Опции таблицы", - "TagCannotBeDeletedWhileInUse": "Невозможно удалить во время использования", - "Tags": "Тэги", - "ApplicationStatusCheckSingleClientMessage": "Листы недоступны из-за ошибок: {0}", + "TableOptions": "Параметры таблицы", + "TagCannotBeDeletedWhileInUse": "Удаление невозможно во время использования", + "Tags": "Теги", + "ApplicationStatusCheckSingleClientMessage": "Приложения недоступны из-за ошибок: {0}", "EditIndexer": "Редактировать индексатор", - "MaintenanceRelease": "Техническая версия: исправления ошибок и другие улучшения. См. Историю коммитов Github для более подробной информации", + "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.", "Filters": "Фильтры", "HistoryCleanupDaysHelpText": "Установите 0, чтобы отключить автоматическую очистку", - "HistoryCleanupDaysHelpTextWarning": "Файлы в корзине старше указанного количества дней будут очищены автоматически", - "OnApplicationUpdateHelpText": "О обновлении приложения", - "OnGrab": "При захвате", - "OnHealthIssue": "О проблемах в системе", - "OnApplicationUpdate": "О обновлении приложения", + "HistoryCleanupDaysHelpTextWarning": "Элементы истории, старше указанного количества дней, будут автоматически удалены", + "OnApplicationUpdateHelpText": "При обновлении приложения", + "OnGrab": "При захвате релиза", + "OnHealthIssue": "При проблемах с состоянием", + "OnApplicationUpdate": "При обновлении приложения", "TestAllIndexers": "Тестировать все индексаторы", - "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent, представленный приложением, который вызывает API", - "NotificationTriggersHelpText": "Выберите, какие события должны вызвать это уведомление", + "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent, предоставленный приложением, вызвавшим API", + "NotificationTriggersHelpText": "Выберите события, которые должны вызвать это уведомление", "NetCore": ".NET", - "GrabReleases": "Захватить релиз", + "GrabReleases": "Захватить релиз(ы)", "UnableToLoadIndexers": "Не удалось загрузить индексаторы", - "Link": "Ссылки", - "MappedDrivesRunningAsService": "Подключённые сетевые диски недоступны, если программа запущена как сервис. Обратитесь в FAQ за дополнительной информацией", + "Link": "Ссылка", + "MappedDrivesRunningAsService": "Сопоставленные сетевые диски недоступны при запуске в качестве службы Windows. См. FAQ для получения дополнительной информации", "No": "Нет", "Yes": "Да", - "AddRemoveOnly": "Добавить и Удалить Только", - "AddNewIndexer": "Добавить Новый Индексатор", - "AddToDownloadClient": "Добавить выпуск в Загрузочный клиент", - "AddedToDownloadClient": "Выпуск добавлен в клиент", - "AddDownloadClientToProwlarr": "Добавление клиента загрузки позволяет {appName} отправлять выпуски прямо из пользовательского интерфейса, выполняя поиск вручную.", - "AddIndexerProxy": "Добавить индексатор прокси", - "Connect": "Оповещения", - "Notification": "Оповещения", + "AddRemoveOnly": "Добавить и удалить только", + "AddNewIndexer": "Добавить новый индексатор", + "AddToDownloadClient": "Добавить релиз в клиент загрузки", + "AddedToDownloadClient": "Релиз добавлен в клиент", + "AddDownloadClientToProwlarr": "Добавление клиента загрузки позволяет {appName} отправлять релизы напрямую из пользовательского интерфейса, во время ручного поиска.", + "AddIndexerProxy": "Добавить прокси для индексатора", + "Connect": "Уведомления", + "Notification": "Уведомление", "Encoding": "Кодирование", "Applications": "Приложения", "Application": "Приложения", - "Notifications": "Оповещения", + "Notifications": "Уведомления", "InstanceName": "Имя экземпляра", - "InstanceNameHelpText": "Имя экземпляра на вкладке и для имени приложения системного журнала", + "InstanceNameHelpText": "Имя экземпляра на вкладке и имя приложения системного журнала", "Started": "Запущено", "Database": "База данных", "Duration": "Длительность", - "ApplicationLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {0}", - "Ended": "Закончился", + "ApplicationLongTermStatusCheckSingleClientMessage": "Все приложения недоступны из-за ошибок более 6 часов: {0}", + "Ended": "Завершён", "LastExecution": "Последнее выполнение", - "NextExecution": "Следующее выполнение", + "NextExecution": "Следующий запуск", "Queued": "В очереди", - "ApplicationLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов", + "ApplicationLongTermStatusCheckAllClientMessage": "Все приложения недоступны из-за ошибок более 6 часов", "LastDuration": "Последняя длительность", - "ThemeHelpText": "Измените тему пользовательского интерфейса приложения, тема «Авто» будет использовать тему вашей ОС для установки светлого или темного режима. Вдохновленный Theme.Park", + "ThemeHelpText": "Изменить тему интерфейса приложения. Тема 'Авто' будет использовать тему вашей ОС для выбора светлого или тёмного режима. Вдохновлено {inspiredBy}.", "Remove": "Удалить", "Replace": "Заменить", - "TheLatestVersionIsAlreadyInstalled": "Последняя версия {appName} уже установлена", + "OnLatestVersion": "Последняя версия {appName} уже установлена", "ApplicationURL": "URL-адрес приложения", "ApplicationUrlHelpText": "Внешний URL-адрес этого приложения, включая http(s)://, порт и базовый URL-адрес", - "Label": "Ярлык", + "Label": "Метка", "ApplyChanges": "Применить изменения", - "ApplyTagsHelpTextRemove": "Удалить: удалить введенные теги", - "DeleteSelectedApplicationsMessageText": "Вы уверены что хотите удалить индексер '{0}'?", + "ApplyTagsHelpTextRemove": "Удалить: Удалить введённые теги", + "DeleteSelectedApplicationsMessageText": "Вы уверены, что хотите удалить выбранные приложения: {count}?", "DeleteSelectedDownloadClients": "Удалить клиент(ы) загрузки", - "DeleteSelectedDownloadClientsMessageText": "Вы уверены, что хотите удалить {count} выбранных клиента загрузки?", - "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить {count} выбранных индексатора?", - "DownloadClientPriorityHelpText": "Установите приоритет нескольких клиентов загрузки. Круговой алгоритм используется для клиентов с таким же приоритетом.", + "DeleteSelectedDownloadClientsMessageText": "Вы уверены, что хотите удалить выбранные клиенты загрузки: {count}?", + "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить выбранные индексаторы: {count}?", + "DownloadClientPriorityHelpText": "Установить приоритет для нескольких клиентов загрузки. Клиенты с одинаковым приоритетом обрабатываются по круговой системе.", "EditSelectedDownloadClients": "Редактировать выбранные клиенты загрузки", - "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {0} символов. Вы можете сделать это через настройки или файл конфигурации", - "Genre": "Жанры", + "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {length} символов в длину. Вы можете сделать это через настройки или файл конфигурации", + "Genre": "Жанр", "Theme": "Тема", "Year": "Год", - "ApplyTagsHelpTextAdd": "Добавить: Добавьте теги в существующий список тегов", - "ApplyTagsHelpTextHowToApplyApplications": "Как добавить ярлыки к выбранным фильмам", + "ApplyTagsHelpTextAdd": "Добавить: Добавить теги к существующему списку тегов", + "ApplyTagsHelpTextHowToApplyApplications": "Как применить теги к выбранным приложениям", "ApplyTagsHelpTextHowToApplyIndexers": "Как применить теги к выбранным индексаторам", - "ApplyTagsHelpTextReplace": "Заменить: заменить теги введенными тегами (оставьте поле пустым, чтобы удалить все теги)", - "Track": "След", - "UpdateAvailable": "Доступно новое обновление", - "More": "Более", + "ApplyTagsHelpTextReplace": "Заменить: Заменить теги введёнными тегами (оставьте поле пустым, чтобы удалить все теги)", + "Track": "Трек", + "UpdateAvailableHealthCheckMessage": "Доступно новое обновление: {version}", + "More": "Ещё", "Publisher": "Издатель", - "ConnectionLostReconnect": "Radarr попытается соединиться автоматически или нажмите кнопку внизу.", - "ConnectionLostToBackend": "Radarr потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", + "ConnectionLostReconnect": "{appName} попытается соединиться автоматически или нажмите кнопку ниже.", + "ConnectionLostToBackend": "{appName} потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.", "RecentChanges": "Последние изменения", "WhatsNew": "Что нового?", - "minutes": "Минуты", - "DeleteAppProfileMessageText": "Вы действительно хотите удалить профиль качества {0}", + "minutes": "минуты", + "DeleteAppProfileMessageText": "Вы уверены, что хотите удалить профиль приложения '{name}'?", "EditDownloadClientImplementation": "Редактировать клиент загрузки - {implementationName}", "EditIndexerImplementation": "Редактировать индексатор - {implementationName}", "NoIndexersFound": "Индексаторы не найдены", - "AuthenticationRequiredHelpText": "Отредактируйте, для каких запросов требуется аутентификация. Не меняйте, пока не поймете все риски.", + "AuthenticationRequiredHelpText": "Отредактируйте, для каких запросов требуется авторизация. Не изменяйте, если не понимаете риски.", "AuthenticationRequired": "Требуется авторизация", - "CountDownloadClientsSelected": "{count} выбранных клиентов загрузки", - "CountIndexersSelected": "{count} выбранных индексаторов", + "CountDownloadClientsSelected": "Выбрано клиентов загрузки: {count}", + "CountIndexersSelected": "Выбрано индексаторов: {count}", "EditSelectedIndexers": "Редактировать выбранный индексатор", - "OnHealthRestored": "При восстановлении работоспособности", - "OnHealthRestoredHelpText": "При восстановлении работоспособности", + "OnHealthRestored": "При восстановлении состояния", + "OnHealthRestoredHelpText": "При восстановлении состояния", "Implementation": "Реализация", - "NoDownloadClientsFound": "Клиенты для загрузки не найдены", - "IndexerDownloadClientHealthCheckMessage": "Индексаторы с недопустимыми клиентами загрузки: {0}.", - "StopSelecting": "Прекратить выбор", + "NoDownloadClientsFound": "Клиенты загрузки не найдены", + "IndexerDownloadClientHealthCheckMessage": "Индексаторы с недопустимыми клиентами загрузки: {indexerNames}.", + "StopSelecting": "Отменить выбор", "AddDownloadClientImplementation": "Добавить клиент загрузки - {implementationName}", "AddConnection": "Добавить подключение", "AddConnectionImplementation": "Добавить подключение - {implementationName}", - "ManageDownloadClients": "Менеджер клиентов загрузки", - "NotificationStatusAllClientHealthCheckMessage": "Все уведомления недоступны из-за сбоев", - "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {0}", + "ManageDownloadClients": "Управление клиентами загрузки", + "NotificationStatusAllClientHealthCheckMessage": "Все уведомления недоступны из-за ошибок", + "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за ошибок: {notificationNames}", "AddIndexerImplementation": "Добавить индексатор - {implementationName}", - "AddIndexerProxyImplementation": "Добавить индексатор - {implementationName}", - "EditConnectionImplementation": "Добавить соединение - {implementationName}", - "EditIndexerProxyImplementation": "Редактировать индексатор - {implementationName}", + "AddIndexerProxyImplementation": "Добавить прокси для индексатора - {implementationName}", + "EditConnectionImplementation": "Редактировать соединение - {implementationName}", + "EditIndexerProxyImplementation": "Редактировать прокси индексатора - {implementationName}", "Season": "Сезон", - "AddApplicationImplementation": "Добавить соединение - {implementationName}", - "EditApplicationImplementation": "Редактировать уведомление - {implementationName}", - "AuthBasic": "Базовый (всплывающее окно браузера)", + "AddApplicationImplementation": "Добавить приложение - {implementationName}", + "EditApplicationImplementation": "Редактировать приложение - {implementationName}", + "AuthBasic": "Базовый (Всплывающее окно браузера)", "AuthForm": "Формы (Страница авторизации)", "DisabledForLocalAddresses": "Отключено для локальных адресов", - "None": "Ничто", - "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить Ваш API ключ?", + "None": "Ничего", + "ResetAPIKeyMessageText": "Вы уверены, что хотите сбросить ключ API?", "Categories": "Категории", - "Album": "альбом", + "Album": "Альбом", "AddCustomFilter": "Добавить специальный фильтр", "AuthenticationMethod": "Способ авторизации", "AuthenticationRequiredPasswordHelpTextWarning": "Введите новый пароль", "AuthenticationRequiredUsernameHelpTextWarning": "Введите новое имя пользователя", - "RestartProwlarr": "Перезапустить {appName}" + "RestartProwlarr": "Перезапустить {appName}", + "AuthenticationRequiredWarning": "Чтобы предотвратить удалённый доступ без авторизации, {appName} теперь требует включения авторизации. Вы можете опционально отключить авторизацию для локальных адресов.", + "Id": "ID", + "ManageClients": "Управление клиентами", + "CountApplicationsSelected": "Выбрано приложений: {count}", + "IndexerHDBitsSettingsCodecs": "Кодеки", + "IndexerHDBitsSettingsMediums": "Mediums", + "Directory": "Каталог", + "CustomFilter": "Настраиваемый фильтр", + "GrabRelease": "Захватить релиз", + "ManualGrab": "Ручной захват", + "OverrideAndAddToDownloadClient": "Переопределить и добавить в клиент загрузки", + "OverrideGrabModalTitle": "Переопределить и захватить - {title}", + "PrioritySettings": "Приоритет: {priority}", + "SelectDownloadClientModalTitle": "{modalTitle} - Выберите клиент загрузки", + "ProxyValidationBadRequest": "Не удалось протестировать прокси. Код состояния: {statusCode}", + "Default": "По умолчанию", + "AppUpdated": "{appName} обновлён", + "AppUpdatedVersion": "{appName} обновлён до версии `{version}`, для получения последних изменений необходимо перезагрузить {appName}", + "Episode": "Эпизод", + "Donate": "Пожертвовать", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Токен приложения, полученный при создании доступа к Freebox API (например, 'app_token')", + "DownloadClientPneumaticSettingsNzbFolder": "Каталог NZB", + "DownloadClientPneumaticSettingsStrmFolder": "Каталог STRM", + "DownloadClientQbittorrentSettingsContentLayout": "Макет контента", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Загружать последовательно (qBittorrent 4.1.0+)", + "DownloadClientRTorrentSettingsUrlPath": "URL-путь", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение rTorrent по умолчанию", + "DownloadClientSettingsAddPaused": "Добавить приостановленные", + "DownloadClientSettingsInitialState": "Начальное состояние", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу RPC {clientName}, например {url}, по умолчанию — '{defaultUrl}'", + "NotificationsTelegramSettingsIncludeAppName": "Включить {appName} в заголовок", + "Category": "Категория", + "Clone": "Клонировать", + "DefaultNameCopiedProfile": "{name} - Копировать", + "DownloadClientFreeboxSettingsHostHelpText": "Имя хоста или IP-адрес хоста Freebox, по умолчанию — '{url}' (будет работать только если находится в одной сети)", + "DownloadClientFreeboxSettingsPortHelpText": "Порт, используемый для доступа к интерфейсу Freebox, по умолчанию — '{port}'", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Этот каталог должен быть доступен из XBMC", + "DownloadClientQbittorrentSettingsSequentialOrder": "Загружать последовательно", + "DownloadClientSettingsDestinationHelpText": "Ручная установка места загрузки, оставьте пустым для использования значения по умолчанию", + "DownloadClientSettingsInitialStateHelpText": "Начальное состояние для торрентов, добавленных в {clientName}", + "DownloadClientRTorrentSettingsAddStopped": "Добавить остановленные", + "External": "Внешний", + "Destination": "Место назначения", + "BlackholeFolderHelpText": "Каталог, в котором {appName} будет хранить файл {extension}", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Необязательный общий каталог для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Download Station по умолчанию", + "DownloadClientFloodSettingsAdditionalTags": "Дополнительные теги", + "DownloadClientFloodSettingsUrlBaseHelpText": "Добавляет префикс к Flood API, такой как {url}", + "DownloadClientFreeboxSettingsApiUrl": "URL-адрес API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Определите базовый URL-адрес Freebox API с версией API, например '{url}', по умолчанию — '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "ID приложения", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID приложения, полученный при создании доступа к Freebox API (например, 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "Токен приложения", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Добавляет свойства мультимедиа в виде тегов. Подсказки являются примерами.", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Для работы этого параметра требуется версия NzbGet не ниже 16.0", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Файлы .strm в этом каталоге будут импортированы дроном", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Первое и последнее сначала", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Загружать первые и последние части сначала (qBittorrent 4.1.0+)", + "UseSsl": "Использовать SSL", + "AuthenticationMethodHelpTextWarning": "Пожалуйста, выберите допустимый метод авторизации", + "XmlRpcPath": "Путь XML RPC", + "UsenetBlackholeNzbFolder": "Каталог NZB", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Подтвердите новый пароль", + "DownloadClientAriaSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Aria2 по умолчанию", + "DownloadClientSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу {clientName}, например, {url}", + "NoHistoryFound": "История не найдена", + "DownloadClientFloodSettingsTagsHelpText": "Начальные теги загрузки. Чтобы быть распознанным, загрузка должна иметь все начальные теги. Это предотвращает конфликты с несвязанными загрузками.", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Выбрать расположение контента: настроенное в qBittorrent, исходный макет из торрента или всегда создавать подкаталог (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Исходное состояние торрентов, добавленных в qBittorrent. Обратите внимание, что принудительные торренты не соблюдают ограничения на раздачу", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Добавляет префикс к URL-адресу json Deluge, см.: {url}", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "При необходимости добавить к заголовку сообщения префикс {appName}, чтобы различать уведомления от разных приложений", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Использовать защищённое соединение. Смотрите 'Параметры' -> 'Веб-интерфейс' -> 'Использовать HTTPS вместо HTTP' в qBittorrent.", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Включение добавит торренты и магнет-ссылки в rTorrent в остановленном состоянии. Это может привести к повреждению магнет-файлов.", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Путь к конечной точке XMLRPC см. {url}. Обычно это RPC2 или [путь к ruTorrent]{url2} при использовании ruTorrent.", + "DownloadClientSettingsUseSslHelpText": "Использовать защищённое соединение при подключении к {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Необязательное место для сохранения загрузок, оставьте поле пустым, чтобы использовать расположение Transmission по умолчанию", + "IndexerSettingsAdditionalParameters": "Дополнительные параметры", + "IndexerSettingsSeedRatio": "Коэффициент раздачи", + "IndexerSettingsSeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто — используется значение клиента загрузки по умолчанию", + "PasswordConfirmation": "Подтверждение пароля", + "TorrentBlackholeTorrentFolder": "Каталог торрента", + "TorrentBlackholeSaveMagnetFilesHelpText": "Сохранить магнет-ссылку, если файл .torrent недоступен (полезно только в случае, если клиент загрузки поддерживает магнет-ссылки, сохранённые в файле)", + "NotificationsEmailSettingsUseEncryption": "Использовать шифрование", + "NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование.", + "LabelIsRequired": "Требуется метка", + "IndexerSettingsSeedTime": "Время сидирования", + "Mixed": "Смешанный", + "IndexerSettingsApiPath": "Путь API", + "IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пусто — используется значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов", + "InvalidUILanguage": "В вашем пользовательском интерфейсе установлен недопустимый язык. Исправьте его и сохраните настройки", + "IndexerHDBitsSettingsCodecsHelpText": "Если не указано, используются все параметры.", + "IndexerHDBitsSettingsMediumsHelpText": "Если не указано, используются все параметры.", + "IndexerSettingsApiPathHelpText": "Путь к API, обычно {url}", + "TorrentBlackholeSaveMagnetFilesExtension": "Сохранить магнет-файлы с расширением", + "TorrentBlackholeSaveMagnetFiles": "Сохранить магнет-файлы", + "IndexerSettingsCookie": "Cookie", + "SecretToken": "Секретный токен", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Расширение для магнет-ссылок, по умолчанию '.magnet'", + "MinimumSeeders": "Минимум сидеров", + "SeedTime": "Время сидирования", + "SeedRatio": "Коэффициент раздачи", + "days": "дни", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Отклонять хэши торрентов из чёрного списка при захвате", + "Author": "Автор", + "IndexerHDBitsSettingsOriginsHelpText": "Если не указано, используются все параметры.", + "Any": "Любой", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Если торрент заблокирован по хэшу, он может не правильно быть отклонён во время RSS/Поиска для некоторых индексаторов.", + "BuiltIn": "Встроенный", + "ProxyValidationUnableToConnect": "Не удалось подключиться к прокси: {exceptionMessage}. Проверьте журнал ошибок для получения дополнительной информации", + "Script": "Скрипт", + "InfoUrl": "URL-адрес информации", + "PublishedDate": "Дата публикации", + "AllSearchResultsHiddenByFilter": "Все результаты поиска скрыты применённым фильтром.", + "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", + "PackageVersionInfo": "{packageVersion} создан {packageAuthor}", + "TotalUserAgentQueries": "Всего запросов User Agent", + "TotalIndexerSuccessfulGrabs": "Всего успешных захватов индексатора", + "TotalIndexerQueries": "Всего запросов к индексатору", + "TVSearchTypes": "Типы поиска ТВ-программ", + "SyncLevelAddRemove": "Только добавление и удаление: При добавлении или удалении индексаторов из {appName} это приложение будет автоматически обновлено.", + "SyncProfiles": "Профили синхронизации", + "TorznabUrl": "URL-адрес Torznab", + "TotalGrabs": "Всего захватов", + "TotalQueries": "Всего запросов", + "TotalHostGrabs": "Всего захватов с хоста", + "TestAllApps": "Тестировать все приложения", + "TotalUserAgentGrabs": "Всего захватов User Agent", + "SyncProfile": "Профиль синхронизации", + "TotalHostQueries": "Всего запросов к хосту", + "SyncLevelFull": "Полная синхронизация: Будет поддерживать индексаторы этого приложения полностью синхронизированными. Изменения, внесённые в индексаторы {appName}, будут синхронизированы с этим приложением. Любые изменения, внесённые в индексаторы удалённо в этом приложении, будут перезаписаны {appName} при следующей синхронизации.", + "Menu": "Меню", + "Artist": "Исполнитель", + "OnGrabHelpText": "При захвате релиза", + "Book": "Книга", + "LogSizeLimit": "Ограничение размера журнала", + "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", + "AudioSearch": "Поиск аудио", + "MusicSearchTypes": "Типы поиска музыки", + "AreYouSureYouWantToDeleteCategory": "Вы уверены, что хотите удалить сопоставленную категорию?", + "ClearHistory": "Очистить историю", + "Privacy": "Конфиденциальность", + "EnableIndexer": "Включить индексатор", + "EnableRssHelpText": "Включить RSS-канал для индексатора", + "Stats": "Статистика", + "HistoryDetails": "Подробности истории", + "IndexerDisabled": "Индексатор выключен", + "IndexerFailureRate": "Процент неудач индексатора", + "LastFailure": "Последняя ошибка", + "MassEditor": "Пакетный редактор", + "FullSync": "Полная синхронизация", + "IndexerAuth": "Авторизация индексатора", + "AddApplication": "Добавить приложение", + "AddSyncProfile": "Добавить профиль синхронизации", + "MappedCategories": "Сопоставленные категории", + "Redirect": "Перенаправление", + "ElapsedTime": "Прошедшее время", + "FilterPlaceHolder": "Поисковые индексаторы", + "Private": "Конфиденциальный", + "AdvancedSettingsHiddenClickToShow": "Расширенные настройки скрыты, нажмите, чтобы показать", + "BasicSearch": "Базовый поиск", + "IndexerStatus": "Статус индексатора", + "ManageApplications": "Управление приложениями", + "PackSeedTime": "Время раздачи пакета", + "SearchAllIndexers": "Поиск во всех индексаторах", + "SettingsSqlLoggingHelpText": "Журналировать все SQL-запросы из {appName}", + "DeleteAppProfile": "Удалить профиль приложения", + "EditSyncProfile": "Редактировать профиль синхронизации", + "EnabledRedirected": "Включено, Перенаправлено", + "IndexerProxies": "Прокси индексатора", + "InitialFailure": "Начальный сбой", + "GoToApplication": "Перейти к приложению", + "IndexerName": "Название индексатора", + "NoSearchResultsFound": "Не обнаружены результаты поиска, повторите поиск ниже.", + "QueryOptions": "Параметры запроса", + "SearchTypes": "Типы поиска", + "DeleteIndexerProxy": "Удалить прокси индексатора", + "DeleteSelectedApplications": "Удалить выбранные приложения", + "DevelopmentSettings": "Настройки разработки", + "HistoryCleanup": "Очистка истории", + "SearchCountIndexers": "Поиск в {count} индексаторе(ах)", + "SelectIndexers": "Выберите индексаторы", + "IndexerBeyondHDSettingsApiKeyHelpText": "Ключ API с сайта (Находится в Моя безопасность => Ключ API)", + "IndexerBeyondHDSettingsSearchTypes": "Типы поиска", + "IndexerBeyondHDSettingsSearchTypesHelpText": "Выберите типы релизов, которые вас интересуют. Если ничего не выбрано, используются все варианты.", + "IndexerFileListSettingsPasskeyHelpText": "Passkey сайта (Это алфавитно-цифровая строка в URL трекера, отображаемая в вашем клиенте загрузки)", + "IndexerGazelleGamesSettingsSearchGroupNames": "Поиск названий групп", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "User-Agent, используемый с cookie из браузера", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "AreYouSureYouWantToDeleteIndexer": "Вы уверены, что хотите удалить '{name}' из {appName}?", + "BookSearch": "Поиск книг", + "BookSearchTypes": "Типы поиска книг", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerSettingsGrabLimitHelpText": "Максимальное количество захватов, указанное соответствующей единицей, которое {appName} будет разрешать для сайта", + "IndexerSettingsLimitsUnit": "Единица лимита", + "IndexerSettingsLimitsUnitHelpText": "Единица времени для определения лимитов каждого индексатора", + "IndexerSettingsQueryLimit": "Лимит запросов", + "IndexerSettingsRssKey": "Ключ RSS", + "Parameters": "Параметры", + "RedirectHelpText": "Перенаправить входящий запрос на загрузку для индексатора и передать захват напрямую, вместо проксирования запроса через {appName}.", + "SettingsFilterSentryEvents": "Фильтровать события аналитики", + "NotSupported": "Не поддерживается", + "SyncLevel": "Уровень синхронизации", + "UISettingsSummary": "Параметры для людей с ограниченными возможностями: дата, язык и цветовая схема", + "DeleteApplication": "Удалить приложение", + "Description": "Описание", + "IndexerQuery": "Запрос индексатора", + "IndexerSite": "Сайт индексатора", + "IndexerTagsHelpText": "Используйте теги для указания прокси индексатора или приложений, синхронизированных с индексатором.", + "QueryType": "Тип запроса", + "MovieSearch": "Поиск фильма", + "RssFeed": "Лента RSS", + "AppsMinimumSeeders": "Приложения Минимальное количество сидеров", + "ConnectSettingsSummary": "Уведомления и пользовательские скрипты", + "DeleteClientCategory": "Удалить категорию клиента загрузки", + "IndexerAlreadySetup": "Как минимум один индексатор уже настроен", + "IndexerCategories": "Категории индексатора", + "IndexerProxy": "Прокси индексатора", + "IndexerRss": "RSS индексатора", + "Public": "Публичный", + "SettingsLogRotate": "Ротация журналов", + "SettingsLogRotateHelpText": "Максимальное количество сохраняемых файлов в каталоге журналов", + "DisabledUntil": "Отключено до", + "SearchType": "Тип поиска", + "AppSettingsSummary": "Приложения и настройки для управления взаимодействием {appName} с вашими программами PVR", + "SettingsFilterSentryEventsHelpText": "Фильтровать известные события ошибок пользователя, чтобы они не отправлялись в виде аналитики.", + "MovieSearchTypes": "Типы поиска фильма", + "Apps": "Программы", + "Auth": "Авторизация", + "ClearHistoryMessageText": "Вы уверены, что хотите очистить всю историю {appName}?", + "DeleteSelectedIndexer": "Удалить выбранный индексатор", + "DownloadClientCategory": "Категория клиента загрузки", + "IndexerHDBitsSettingsPasskeyHelpText": "Passkey из пользовательских данных", + "SettingsIndexerLogging": "Включить индексатор", + "RepeatSearch": "Повторить поиск", + "AddCategory": "Добавить категорию", + "AdvancedSettingsShownClickToHide": "Расширенные настройки видны, нажмите, чтобы скрыть", + "AppProfileInUse": "Профиль приложения в использовании", + "IndexerAlphaRatioSettingsExcludeScene": "Исключить SCENE", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "Исключить релизы SCENE из результатов", + "IndexerBeyondHDSettingsRewindOnly": "Только повторы", + "IndexerDetails": "Подробности индексатора", + "EditCategory": "Редактировать категорию", + "FoundCountReleases": "Найдено релизов: {itemCount}", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerBeyondHDSettingsLimitedOnly": "Только лимитированные", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Искать только возвраты", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Искать только повторы", + "IndexerHistoryLoadError": "Ошибка загрузки истории индексатора", + "IndexerNebulanceSettingsApiKeyHelpText": "Ключ API из Настроек пользователя > Ключи API. Ключ должен иметь права на просмотр и загрузку", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Ключ API сайта", + "SettingsConsoleLogLevel": "Уровень журналирования консоли", + "IndexerSettingsGrabLimit": "Лимит захвата", + "IndexerSettingsVipExpiration": "Дата окончания VIP", + "SearchQueries": "Запросы поиска", + "SeedRatioHelpText": "Коэффициент, которого должен достичь торрент перед остановкой, пусто - значение по умолчанию приложения", + "NoIndexerCategories": "Нет категорий для этого индексатора", + "IndexerVipExpiredHealthCheckMessage": "Привилегии VIP для индексатора истекли: {indexerNames}", + "DefaultCategory": "Категория по умолчанию", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Показать только релизы с freeleech", + "IndexerHDBitsSettingsOrigins": "Источники", + "IndexerNzbIndexSettingsApiKeyHelpText": "Ключ API сайта", + "IndexerOrpheusSettingsApiKeyHelpText": "API ключ сайта (Находится в Настройки => Настройки доступа)", + "IndexerSettingsApiUser": "Пользователь API", + "IndexerSettingsAppsMinimumSeeders": "Приложения Минимальное количество сидеров", + "RawSearchSupported": "Поддерживается необработанный поиск", + "RssQueries": "Запросы RSS", + "SearchCapabilities": "Возможности поиска", + "SearchIndexers": "Поисковые индексаторы", + "ActiveIndexers": "Активные индексаторы", + "ApplicationsLoadError": "Не удалось загрузить список приложений", + "IncludeManualGrabsHelpText": "Включить ручные захваты, сделанные внутри {appName}", + "NoApplicationsFound": "Приложения не найдены", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Выберите этот вариант, если хотите использовать имена файлов торрента в качестве названий релизов", + "IndexerHDBitsSettingsUsernameHelpText": "Имя пользователя сайта", + "IndexerSettingsCookieHelpText": "Cookie сайта", + "IndexerSettingsFreeleechOnly": "Только с freeleech", + "IndexerSettingsPasskey": "Pass Key", + "IndexerId": "ID индексатора", + "DeleteSelectedIndexers": "Удалить выбранные индексаторы", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Требуются разрешения 'Пользователь' и 'Торренты'", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Дополнительные параметры Newznab", + "IndexerNewznabSettingsApiKeyHelpText": "Ключ API сайта", + "IndexerSettingsBaseUrl": "Базовый URL-адрес", + "IndexerSettingsAppsMinimumSeedersHelpText": "Минимальное количество сидеров, необходимое для приложений, чтобы индексатор мог скачать, пустое значение - значение профиля синхронизации по умолчанию", + "PackSeedTimeHelpText": "Время, в течение которого торрент пакета (сезон или дискография) должен оставаться на раздаче перед остановкой, пусто — используется значение приложения по умолчанию", + "IndexerNoDefinitionCheckHealthCheckMessage": "Индексаторы {indexerNames} не имеют определения и не работают. Удалите их из {appName} и добавьте снова.", + "AuthQueries": "Запросы авторизации", + "IndexerNewznabSettingsVipExpirationHelpText": "Введите дату (yyyy-mm-dd) окончания VIP или оставьте поле пустым, {appName} оповестит вас за неделю до окончания VIP", + "IndexerPassThePopcornSettingsApiUserHelpText": "Эти настройки расположены в разделе безопасности PassThePopcorn (Редактировать профиль > Безопасность).", + "IndexerSettingsBaseUrlHelpText": "Выберите основной URL-адрес, который {appName} будет использовать для запросов на сайт", + "Open": "Открыть", + "IndexerInfo": "Информация об индексаторе", + "SettingsLogSql": "Журналировать SQL", + "AverageResponseTimesMs": "Среднее время ответа индексатора (мс)", + "CountIndexersAvailable": "Доступно индексаторов: {count}", + "IndexerSettingsSummary": "Конфигурация глобальных настроек индексатора, включая прокси.", + "IndexerTagsHelpTextWarning": "Теги следует использовать с осторожностью, они могут иметь непредвиденные последствия. Индексатор с тегом будет синхронизироваться только с приложениями, имеющими тот же тег.", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Ключ API сайта", + "NoIndexerHistory": "Нет истории для этого индексатора", + "AppsMinimumSeedersHelpText": "Минимальное количество сидеров, необходимое для приложений, чтобы индексатор мог скачать, пустое значение - значение профиля синхронизации по умолчанию", + "DownloadClientsSettingsSummary": "Настройки клиентов загрузки для интеграции в интерфейс поиска {appName}", + "Proxies": "Прокси", + "AppProfileSelectHelpText": "Профили приложения используются для управления настройками RSS, автоматического поиска и интерактивного поиска при синхронизации приложения", + "ProwlarrSupportsAnyDownloadClient": "{appName} совместим с любым из перечисленных ниже клиентом загрузки.", + "Query": "Запрос", + "QueryResults": "Результаты запроса", + "ActiveApps": "Активные приложения", + "ApplicationTagsHelpText": "Синхронизировать индексаторы с этим приложением, которые имеют один или более совпадающих тегов. Если здесь не перечислены теги, то все индексаторы будут синхронизированы без ограничений.", + "ApplicationTagsHelpTextWarning": "Теги следует использовать с осторожностью, они могут иметь непредвиденные последствия. Приложение с тегом будет синхронизироваться только с индексаторами, имеющими тот же тег.", + "IndexerGazelleGamesSettingsApiKeyHelpText": "API ключ сайта (Находится в Настройки => Настройки доступа)", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Поиск релизов по названию групп", + "IndexerHDBitsSettingsUseFilenames": "Использовать имена файлов", + "IndexerRedactedSettingsApiKeyHelpText": "API ключ сайта (Находится в Настройки => Настройки доступа)", + "IndexerSettingsPackSeedTime": "Время раздачи пакета", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Время, в течение которого торрент пакета (сезон или дискография) должен оставаться на раздаче перед остановкой, пусто — используется значение приложения по умолчанию", + "ClickToChangeQueryOptions": "Нажмите, чтобы изменить параметры запроса", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Категория по умолчанию, если для релиза нет соответствующей категории. Создание определённой категории для {appName}, позволяет избежать конфликтов с загрузками, не связанными с {appName}. Использование категории не обязательно, но настоятельно рекомендуется. Создаёт подкаталог [category] в каталоге вывода.", + "AverageQueries": "Среднее количество запросов", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerMTeamTpSettingsApiKeyHelpText": "Ключ API сайта (Находится в Панели управления пользователя => Безопасность => Лаборатория)", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "ProwlarrDownloadClientsAlert": "Если вы намерены выполнять поиск непосредственно в {appName}, вам необходимо добавить клиенты загрузки. В противном случае, добавлять их здесь не нужно. Для поиска из ваших приложений используются клиенты загрузки, которые настроены в самих приложениях.", + "ProwlarrDownloadClientsInAppOnlyAlert": "Клиенты загрузки предназначены только для поиска внутри приложения {appName} и не синхронизируются с другими приложениями. Мы не планируем добавлять функцию синхронизации.", + "ProwlarrSupportsAnyIndexer": "{appName} поддерживает множество индексаторов, а также любой индексатор, использующий стандарт Newznab/Torznab, с помощью 'Generic Newznab' (для Usenet) или 'Generic Torznab' (для торрентов). Выберите и добавьте свой индексатор из списка ниже.", + "Redirected": "Перенаправлено", + "DownloadClientSettingsDefaultCategoryHelpText": "Категория по умолчанию, если для релиза нет соответствующей категории. Создание определённой категории для {appName}, позволяет избежать конфликтов с загрузками, не связанными с {appName}. Использование категории не обязательно, но настоятельно рекомендуется.", + "DownloadClientSettingsPriorityItemHelpText": "Приоритет, используемый при захвате элементов", + "GrabTitle": "Захватить название", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Искать только с freeleech (Лимитированная отдача)", + "IndexerBeyondHDSettingsRefundOnly": "Только возврат", + "IndexerBeyondHDSettingsRssKeyHelpText": "Ключ RSS с сайта (Находится в Моя безопасность => Ключ RSS)", + "IndexerDownloadClientHelpText": "Определите клиент загрузки, используемый для загрузки из этого индексатора в {appName}.", + "NewznabUrl": "URL-адрес Newznab", + "SeedTimeHelpText": "Время, в течение которого торрент должен оставаться на раздаче перед остановкой, пусто — используется значение приложения по умолчанию", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerFileListSettingsUsernameHelpText": "Имя пользователя сайта", + "IndexerHealthCheckNoIndexers": "Нет включённых индексаторов, {appName} не будет возвращать результаты поиска", + "IndexerObsoleteCheckMessage": "Индексаторы: {0} устарели или были обновлены. Удалите их из {appName} и (или) добавьте снова", + "IndexerSettingsQueryLimitHelpText": "Максимальное количество запросов, указанное соответствующей единицей, которое {appName} будет разрешать для сайта", + "IndexerVipExpiringHealthCheckMessage": "Привилегии VIP для индексатора скоро истекут: {indexerNames}", + "MinimumSeedersHelpText": "Минимальное количество сидеров, необходимое приложению для захвата индексатором", + "SelectedCountOfCountReleases": "Выбрано {selectedCount} из {itemCount} релизов", + "SemiPrivate": "Частично приватный", + "SettingsIndexerLoggingHelpText": "Журналировать дополнительные данные индексатора, включая ответ", + "SyncAppIndexers": "Синхронизировать индексаторы приложения", + "UnableToLoadAppProfiles": "Не удалось загрузить профили приложения", + "Website": "Веб-сайт", + "TvSearch": "Поиск ТВ-программ", + "Url": "URL-адрес", + "UnableToLoadIndexerProxies": "Не удалось загрузить прокси индексатора", + "UnableToLoadDevelopmentSettings": "Не удалось загрузить настройки разработки", + "VipExpiration": "Дата окончания VIP", + "IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent", + "AverageGrabs": "Среднее количество захватов", + "IndexerSettingsPreferMagnetUrl": "Предпочитать Magnet URL", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Только Golden Popcorn", + "IndexerSettingsPreferMagnetUrlHelpText": "При включении этот индексатор предпочтёт использовать для загрузки magnet URL, с возможностью перехода на торрент-ссылки", + "IndexerAvistazSettingsPasswordHelpText": "Пароль веб-сайта", + "IndexerAvistazSettingsPidHelpText": "PID со страницы Мой аккаунт или Мой профиль", + "IndexerAvistazSettingsUsernameHelpTextWarning": "API этого индексатора доступен только для участников и выше рангом.", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Искать релизы только Golden Popcorn", + "PreferMagnetUrl": "Предпочитать Magnet URL", + "PreferMagnetUrlHelpText": "При включении этот индексатор предпочтёт использовать для загрузки magnet URL, с возможностью перехода на торрент-ссылки", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Искать только релизы с freeleech", + "IndexerAvistazSettingsUsernameHelpText": "Имя пользователя сайта", + "AptUpdater": "Использовать apt для установки обновления", + "Download": "Загрузить", + "ErrorRestoringBackup": "Ошибка восстановления резервной копии", + "FailedToFetchUpdates": "Не удалось загрузить обновления", + "LogFilesLocation": "Файлы журнала расположены в: {location}", + "Logout": "Завершить сеанс", + "UpdateAppDirectlyLoadError": "Обновление {appName} напрямую невозможно,", + "DockerUpdater": "Обновите контейнер Docker, чтобы получить обновление", + "ExternalUpdater": "{appName} настроен на использование внешнего механизма обновления", + "NoEventsFound": "Событий не найдено", + "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит интерфейс пользователя во время процесса восстановления.", + "TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Информация' и может быть изменён в [Общих настройках](/settings/general)", + "UpdaterLogFiles": "Файлы журнала обновления", + "WouldYouLikeToRestoreBackup": "Хотите восстановить резервную копию '{name}'?", + "Install": "Установить", + "InstallLatest": "Установить последнюю", + "InstallMajorVersionUpdateMessageLink": "Для получения дополнительной информации посетите [{domain}]({url}).", + "InstallMajorVersionUpdate": "Установить обновление", + "InstallMajorVersionUpdateMessage": "Это обновление установит новую версию, которая может не поддерживаться вашей системой. Вы уверены, что хотите установить это обновление?", + "FailedToFetchSettings": "Не удалось загрузить настройки", + "CurrentlyInstalled": "Установлено", + "PreviouslyInstalled": "Ранее установленный", + "DownloadClientUTorrentProviderMessage": "Мы настоятельно советуем не использовать uTorrent, т.к. он известен как программа-шифровальщик и в целом вредоносное ПО." } diff --git a/src/NzbDrone.Core/Localization/Core/sk.json b/src/NzbDrone.Core/Localization/Core/sk.json index 926f6ada0..f9fc30a55 100644 --- a/src/NzbDrone.Core/Localization/Core/sk.json +++ b/src/NzbDrone.Core/Localization/Core/sk.json @@ -35,7 +35,7 @@ "DeleteNotificationMessageText": "Naozaj chcete zmazať značku formátu {0} ?", "DeleteTagMessageText": "Naozaj chcete zmazať značku formátu {0} ?", "AllIndexersHiddenDueToFilter": "Všetky filmy sú skryté kvôli použitému filtru.", - "AnalyticsEnabledHelpText": "Odosielajte anonymné informácie o používaní a chybách na servery {appName}u. To zahŕňa informácie o vašom prehliadači, ktoré stránky {appName} WebUI používate, hlásenia chýb a taktiež verziu operačného systému a spúšťacieho prostredia. Tieto informácie použijeme k uprednostňovaniu funkcií a oprav chýb.", + "AnalyticsEnabledHelpText": "Odosielajte anonymné informácie o používaní a chybách na servery aplikácie {appName}. Zahŕňa to informácie o vašom prehliadači, ktoré stránky webového používateľského rozhrania {appName} používate, hlásenia chýb, ako aj verziu operačného systému a spustenia. Tieto informácie použijeme na stanovenie priorít funkcií a opráv chýb.", "AuthenticationMethodHelpText": "Vyžadovať používateľské meno a heslo pre prístup k {appName}u", "BackupFolderHelpText": "Relatívne cesty budú v priečinku AppData {appName}u", "BranchUpdate": "Vetva, ktorá sa má použiť k aktualizácií {appName}u", @@ -100,13 +100,13 @@ "Indexer": "Indexer", "New": "Nový", "Queued": "Fronta", - "RSS": "RSS", + "Rss": "RSS", "Remove": "Odstrániť", "Replace": "Nahradiť", "ApplicationURL": "URL aplikácie", "ApplicationUrlHelpText": "Externá URL tejto aplikácie vrátane http(s)://, portu a URL základu", "Label": "Štítok", - "ApplyTagsHelpTextAdd": "Pridať: Pridajte značky do existujúceho zoznamu značiek", + "ApplyTagsHelpTextAdd": "Pridať: Pridať značky do existujúceho zoznamu značiek", "ApplyTagsHelpTextRemove": "Odobrať: Odoberie zadané značky", "ApplyTagsHelpTextReplace": "Nahradiť: Nahradí značky zadanými značkami (pre vymazanie všetkých značiek, nezadávajte žiadne)", "ApplyTagsHelpTextHowToApplyApplications": "Ako použiť značky na vybrané filmy", @@ -117,5 +117,36 @@ "AuthBasic": "Základné (vyskakovacie okno prehliadača)", "AuthForm": "Formuláre (prihlasovacia stránka)", "DisabledForLocalAddresses": "Zakázané pre miestne adresy", - "ResetAPIKeyMessageText": "Naozaj chcete obnoviť kľúč API?" + "ResetAPIKeyMessageText": "Naozaj chcete obnoviť kľúč API?", + "CountApplicationsSelected": "{count} Označených kolekcií", + "Id": "ID", + "EnableSslHelpText": " Vyžaduje sa reštart s oprávnením správcu, aby sa zmeny prejavili", + "RestartRequiredHelpTextWarning": "Vyžaduje sa reštart, aby sa zmeny prejavili", + "Album": "Album", + "AddConnection": "Pridať podmienku", + "AddConnectionImplementation": "Pridať pripojenie - {implementationName}", + "AddDownloadClientImplementation": "Pridať klienta pre sťahovanie - {implementationName}", + "AddIndexerImplementation": "Pridať Indexer - {implementationName}", + "UnableToAddANewDownloadClientPleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "UnableToAddANewNotificationPleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "UnableToAddANewIndexerPleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "BackupsLoadError": "Nie je možné načítať albumy", + "Docker": "Docker", + "UnableToAddANewApplicationPleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "EditIndexerImplementation": "Pridať Indexer - {implementationName}", + "AddApplicationImplementation": "Pridať pripojenie - {implementationName}", + "EditConnectionImplementation": "Pridať pripojenie - {implementationName}", + "EditDownloadClientImplementation": "Pridať klienta pre sťahovanie - {implementationName}", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "Directory": "Priečinok", + "IndexerHDBitsSettingsCodecs": "Kodek", + "AddIndexerProxyImplementation": "Pridať Indexer - {implementationName}", + "EditIndexerProxyImplementation": "Pridať Indexer - {implementationName}", + "EditApplicationImplementation": "Pridať podmienku - {implementationName}", + "UnableToAddANewAppProfilePleaseTryAgain": "Nie je možné pridať novú podmienku, skúste to znova.", + "BuiltIn": "Vstavaný", + "AllSearchResultsHiddenByFilter": "Použitý filter skryje všetky výsledky", + "AptUpdater": "Použiť apt pre inštaláciu aktualizácie", + "Discord": "Discord", + "Clone": "Zatvoriť" } diff --git a/src/NzbDrone.Core/Localization/Core/sv.json b/src/NzbDrone.Core/Localization/Core/sv.json index a61b5e976..9e7ee1c6c 100644 --- a/src/NzbDrone.Core/Localization/Core/sv.json +++ b/src/NzbDrone.Core/Localization/Core/sv.json @@ -1,8 +1,8 @@ { "About": "Om", "Language": "Språk", - "IndexerStatusCheckSingleClientMessage": "Indexerare otillgängliga på grund av fel: {0}", - "IndexerStatusCheckAllClientMessage": "Samtliga indexerare otillgängliga på grund av fel", + "IndexerStatusUnavailableHealthCheckMessage": "Indexerare otillgängliga på grund av fel: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Samtliga indexerare otillgängliga på grund av fel", "Indexers": "Indexerare", "Host": "Värd", "History": "Historik", @@ -13,10 +13,10 @@ "Filter": "Filter", "Files": "Filer", "Events": "Händelser", - "Edit": "Redigera", - "DownloadClientStatusCheckSingleClientMessage": "Otillgängliga nedladdningsklienter på grund av misslyckade anslutningsförsök: {0}", - "DownloadClientStatusCheckAllClientMessage": "Samtliga nedladdningsklienter är otillgängliga på grund av misslyckade anslutningsförsök", - "DownloadClients": "Nedladdningsklienter", + "Edit": "Ändra", + "DownloadClientStatusSingleClientHealthCheckMessage": "Otillgängliga nedladdningsklienter på grund av misslyckade anslutningsförsök: {downloadClientNames}", + "DownloadClientStatusAllClientHealthCheckMessage": "Samtliga nedladdningsklienter är otillgängliga på grund av misslyckade anslutningsförsök", + "DownloadClients": "Nerladdningsklienter", "Delete": "Radera", "Dates": "Datum", "Date": "Datum", @@ -34,9 +34,9 @@ "LogFiles": "Loggfiler", "View": "Vy", "Updates": "Uppdateringar", - "UpdateCheckUINotWritableMessage": "Ej möjligt att installera uppdatering då användargränssnittsmappen '{0}' inte är skrivbar för användaren '{1}'.", - "UpdateCheckStartupTranslocationMessage": "Ej möjligt att installera uppdatering då uppstartsmappen '{0}' är i en \"App translocation\"-mapp.", - "UpdateCheckStartupNotWritableMessage": "Ej möjligt att installera uppdatering då uppstartsmappen '{0}' inte är skrivbar för användaren '{1}'.", + "UpdateUiNotWritableHealthCheckMessage": "Ej möjligt att installera uppdatering då användargränssnittsmappen '{uiFolder}' inte är skrivbar för användaren '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Ej möjligt att installera uppdatering då uppstartsmappen '{startupFolder}' är i en \"App translocation\"-mapp.", + "UpdateStartupNotWritableHealthCheckMessage": "Ej möjligt att installera uppdatering då uppstartsmappen '{startupFolder}' inte är skrivbar för användaren '{userName}'.", "UnselectAll": "Avmarkera samtliga", "UISettingsSummary": "Datum, språk, och färgblindhetsinställningar", "UI": "Användargränssnitt", @@ -60,9 +60,9 @@ "ReleaseBranchCheckOfficialBranchMessage": "Gren {0} är inte en giltig gren av {appName}, du kommer ej erhålla uppdateringar", "Refresh": "Uppdatera", "Queue": "Kö", - "ProxyCheckResolveIpMessage": "Misslyckades att slå upp IP-adressen till konfigurerad proxyvärd {0}", - "ProxyCheckFailedToTestMessage": "Test av proxy misslyckades: {0}", - "ProxyCheckBadRequestMessage": "Test av proxy misslyckades. Statuskod: {0}", + "ProxyResolveIpHealthCheckMessage": "Misslyckades att slå upp IP-adressen till konfigurerad proxyvärd {proxyHostName}", + "ProxyFailedToTestHealthCheckMessage": "Test av proxy misslyckades: {url}", + "ProxyBadRequestHealthCheckMessage": "Test av proxy misslyckades. Statuskod: {statusCode}", "Proxy": "Proxy", "Protocol": "Protokoll", "LastWriteTime": "Senast skriven tid", @@ -105,7 +105,7 @@ "Level": "Nivå", "KeyboardShortcuts": "Tangentbordsgenvägar", "Info": "Info", - "HealthNoIssues": "Inga problem hittades med din konfiguration", + "NoIssuesWithYourConfiguration": "Inga problem hittades med din konfiguration", "Error": "Fel", "ConnectionLost": "Anslutning saknas", "Component": "Komponent", @@ -114,11 +114,11 @@ "Cancel": "Avbryt", "Apply": "TIllämpa", "Age": "Ålder", - "SystemTimeCheckMessage": "Systemklockan går fel med mer än en dag. Schemalagda uppgifter kan få problem att köras innan tiden är korrigerad", + "SystemTimeHealthCheckMessage": "Systemklockan går fel med mer än en dag. Schemalagda uppgifter kan få problem att köras innan tiden är korrigerad", "HomePage": "Hemsida", "IndexerPriority": "Indexerprioritet", "Reddit": "Reddit", - "IndexerProxyStatusCheckAllClientMessage": "Samtliga indexerare otillgängliga på grund av fel", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Samtliga indexerare otillgängliga på grund av fel", "NoLinks": "Inga länkar", "NoTagsHaveBeenAddedYet": "Inga taggar har lagts till ännu", "NotificationTriggers": "Meddelandeutlösare", @@ -126,7 +126,7 @@ "RefreshMovie": "Uppdatera filmen", "RemoveFilter": "Ta bort filter", "ResetAPIKey": "Återställa API-nyckel", - "RSS": "RSS", + "Rss": "RSS", "SettingsEnableColorImpairedMode": "Aktivera färgskadat läge", "ClientPriority": "Klient prioritet", "SettingsTimeFormat": "Tidsformat", @@ -155,7 +155,7 @@ "Result": "Resultat", "Retention": "Bibehållande", "CertificateValidation": "Validering av Certifikat", - "RSSIsNotSupportedWithThisIndexer": "RSS stöds inte av denna indexerare", + "RssIsNotSupportedWithThisIndexer": "RSS stöds inte av denna indexerare", "SaveSettings": "Spara inställningar", "ScriptPath": "Skriptsökväg", "CouldNotConnectSignalR": "Kunde inte ansluta till SignalR, UI uppdateras inte", @@ -169,7 +169,7 @@ "BindAddress": "Bindningsadress", "Branch": "Gren", "CloseCurrentModal": "Stäng nuvarande modal", - "DBMigration": "DB Migration", + "DatabaseMigration": "DB Migration", "DeleteApplicationMessageText": "Är du säker på att du vill radera aviseringen '{0}'?", "Discord": "Discord", "Donations": "Donationer", @@ -196,11 +196,11 @@ "IncludeHealthWarningsHelpText": "Inkludera hälsovarningar", "IndexerFlags": "Indexerflaggor", "AddDownloadClient": "Lägg till nedladdningsklient", - "AddIndexer": "Lägg till indexerare", + "AddIndexer": "Lägg till index", "AddingTag": "Lägg till tagg", "Enabled": "Aktiverad", "Exception": "Undantag", - "IndexerProxyStatusCheckSingleClientMessage": "Indexerare otillgängliga på grund av fel: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Indexerare otillgängliga på grund av fel: {indexerProxyNames}", "UILanguageHelpTextWarning": "Omladdning av webbläsaren krävs", "UnableToLoadHistory": "Det gick inte att läsa in historiken", "Uptime": "Drifttid", @@ -241,8 +241,8 @@ "Grabs": "Hämta", "IgnoredAddresses": "Ignorerade adresser", "IllRestartLater": "Jag startar om senare", - "IndexerLongTermStatusCheckAllClientMessage": "Alla indexerare är inte tillgängliga på grund av fel i mer än 6 timmar", - "IndexerLongTermStatusCheckSingleClientMessage": "Indexatorer är inte tillgängliga på grund av misslyckanden i mer än sex timmar: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Alla indexerare är inte tillgängliga på grund av fel i mer än 6 timmar", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexatorer är inte tillgängliga på grund av misslyckanden i mer än sex timmar: {indexerNames}", "IndexerPriorityHelpText": "Indexeringsprioritet från 1 (högst) till 50 (lägst). Standard: 25.", "InteractiveSearch": "Interaktiv sökning", "Interval": "Intervall", @@ -289,7 +289,7 @@ "Torrent": "Torrenter", "UILanguage": "UI-språk", "UnableToAddANewApplicationPleaseTryAgain": "Det gick inte att lägga till ett nytt meddelande, försök igen.", - "UnableToLoadBackups": "Det gick inte att ladda säkerhetskopior", + "BackupsLoadError": "Det gick inte att ladda säkerhetskopior", "UnableToAddANewIndexerProxyPleaseTryAgain": "Inte möjligt att lägga till en ny indexerare, var god försök igen.", "UnableToLoadGeneralSettings": "Det går inte att läsa in allmänna inställningar", "New": "Ny", @@ -308,7 +308,7 @@ "ApplicationStatusCheckAllClientMessage": "Samtliga listor otillgängliga på grund av fel", "LogLevel": "Loggnivå", "NoLogFiles": "Inga loggfiler", - "UnableToLoadDownloadClients": "Det gick inte att ladda nedladdningsklienter", + "DownloadClientsLoadError": "Det gick inte att ladda nedladdningsklienter", "UpdateMechanismHelpText": "Använd {appName}s inbyggda uppdaterare eller ett skript", "AddDownloadClientToProwlarr": "Lägg till en nedladdningsklient tillåter {appName} att sända nyutgåvor direkt från UI:t samtidigt som en manuell sökning genomförs.", "UnableToLoadIndexerProxies": "Kunde inte ladda Indexer Proxies", @@ -342,8 +342,8 @@ "NoSearchResultsFound": "Inget sökresultat hittat, försök utföra en ny sökning nedan.", "NetCore": ".NET", "MaintenanceRelease": "Underhållsutgåva", - "IndexerVipCheckExpiringClientMessage": "Indexer VIP förmåner utgår snart: {0}", - "IndexerVipCheckExpiredClientMessage": "Indexer VIP förmåner har utgått: {0}", + "IndexerVipExpiringHealthCheckMessage": "Indexer VIP förmåner utgår snart: {indexerNames}", + "IndexerVipExpiredHealthCheckMessage": "Indexer VIP förmåner har utgått: {indexerNames}", "IndexerTagsHelpText": "Använd taggar för att specificera standardklient, specificera Indexer Proxies, eller bara för att organisera dina indexers.", "IndexerSettingsSummary": "Konfigurera flera globala Indexerinställningar, includerat Proxies.", "IndexerRss": "Indexer Rss", @@ -394,7 +394,7 @@ "No": "Nej", "ApplicationLongTermStatusCheckAllClientMessage": "Alla indexerare är inte tillgängliga på grund av fel i mer än 6 timmar", "ApplicationLongTermStatusCheckSingleClientMessage": "Indexatorer är inte tillgängliga på grund av misslyckanden i mer än sex timmar: {0}", - "Duration": "Tid", + "Duration": "Längd", "Ended": "Avslutad", "LastDuration": "lastDuration", "LastExecution": "Senaste avrättningen", @@ -402,7 +402,7 @@ "Queued": "Köad", "Remove": "Ta bort", "Replace": "Ersätta", - "TheLatestVersionIsAlreadyInstalled": "Den senaste versionen av {appName} är redan installerad", + "OnLatestVersion": "Den senaste versionen av {appName} är redan installerad", "ApplicationURL": "Applikations-URL", "ApplicationUrlHelpText": "Denna applikations externa URL inklusive http(s)://, port och URL-bas", "Episode": "Avsnitt", @@ -431,11 +431,35 @@ "RecentChanges": "Senaste ändringar", "WhatsNew": "Vad är nytt?", "minutes": "Minuter", - "NotificationStatusSingleClientHealthCheckMessage": "Listor otillgängliga på grund av fel: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Listor otillgängliga på grund av fel: {notificationNames}", "NotificationStatusAllClientHealthCheckMessage": "Samtliga listor otillgängliga på grund av fel", "AuthBasic": "Grundläggande (Browser Popup)", "AuthForm": "Blanketter (inloggningssida)", "DisabledForLocalAddresses": "Inaktiverad för lokala adresser", "None": "Ingen", - "ResetAPIKeyMessageText": "Är du säker på att du vill nollställa din API-nyckel?" + "ResetAPIKeyMessageText": "Är du säker på att du vill nollställa din API-nyckel?", + "AddCustomFilter": "Lägg till anpassat filter", + "UseSsl": "Använd SSL", + "IndexerHDBitsSettingsMediums": "Medium", + "Directory": "Plats", + "ProxyValidationBadRequest": "Test av proxy misslyckades. Statuskod: {statusCode}", + "CustomFilter": "Anpassade Filter", + "GrabRelease": "Hämta Utågva", + "BuiltIn": "Inbyggd", + "Script": "Skript", + "PublishedDate": "Publiceringsdatum", + "Redirected": "Omdirigera", + "AllSearchResultsHiddenByFilter": "Alla resultat döljs av det tillämpade filtret", + "DockerUpdater": "uppdatera dockerbehållaren för att ta emot uppdateringen", + "ErrorRestoringBackup": "Fel vid återställning av säkerhetskopian", + "NoEventsFound": "Inga händelser hittades", + "RestartReloadNote": "Obs! {appName} startar automatiskt om och laddar om användargränssnittet under återställningsprocessen.", + "UpdateAppDirectlyLoadError": "Det går inte att uppdatera {appName} direkt,", + "AptUpdater": "Använd apt för att installera uppdateringen", + "Download": "Ladda ner", + "ExternalUpdater": "{appName} är konfigurerad för att använda en extern uppdateringsmekanism", + "InstallLatest": "Installera senaste", + "Clone": "Avsluta", + "Mixed": "Fast", + "CurrentlyInstalled": "För närvarande installerad" } diff --git a/src/NzbDrone.Core/Localization/Core/th.json b/src/NzbDrone.Core/Localization/Core/th.json index 42e6c587f..b157c57d2 100644 --- a/src/NzbDrone.Core/Localization/Core/th.json +++ b/src/NzbDrone.Core/Localization/Core/th.json @@ -15,8 +15,8 @@ "PortNumber": "หมายเลขพอร์ต", "Restart": "เริ่มต้นใหม่", "SSLCertPathHelpText": "พา ธ ไปยังไฟล์ pfx", - "IndexerProxyStatusCheckAllClientMessage": "รายการทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", - "IndexerProxyStatusCheckSingleClientMessage": "ตัวจัดทำดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "รายการทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", + "IndexerProxyStatusUnavailableHealthCheckMessage": "ตัวจัดทำดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {indexerProxyNames}", "Indexers": "ดัชนี", "Info": "ข้อมูล", "InteractiveSearch": "การค้นหาแบบโต้ตอบ", @@ -35,7 +35,7 @@ "PendingChangesDiscardChanges": "ยกเลิกการเปลี่ยนแปลงและออก", "PendingChangesMessage": "คุณยังไม่ได้บันทึกการเปลี่ยนแปลงแน่ใจไหมว่าต้องการออกจากหน้านี้", "PendingChangesStayReview": "อยู่และตรวจสอบการเปลี่ยนแปลง", - "ProxyCheckFailedToTestMessage": "ไม่สามารถทดสอบพร็อกซี: {0}", + "ProxyFailedToTestHealthCheckMessage": "ไม่สามารถทดสอบพร็อกซี: {url}", "ProxyPasswordHelpText": "คุณจะต้องป้อนชื่อผู้ใช้และรหัสผ่านหากจำเป็นเท่านั้น เว้นว่างไว้เป็นอย่างอื่น", "ShowAdvanced": "แสดงขั้นสูง", "ShowSearch": "แสดงการค้นหา", @@ -58,8 +58,8 @@ "UILanguageHelpTextWarning": "จำเป็นต้องโหลดเบราว์เซอร์ใหม่", "UnableToAddANewIndexerPleaseTryAgain": "ไม่สามารถเพิ่มตัวสร้างดัชนีใหม่ได้โปรดลองอีกครั้ง", "UnableToAddANewIndexerProxyPleaseTryAgain": "ไม่สามารถเพิ่มตัวสร้างดัชนีใหม่ได้โปรดลองอีกครั้ง", - "UnableToLoadDownloadClients": "ไม่สามารถโหลดไคลเอนต์ดาวน์โหลด", - "UpdateCheckUINotWritableMessage": "ไม่สามารถติดตั้งการอัปเดตเนื่องจากโฟลเดอร์ UI \"{0}\" ไม่สามารถเขียนได้โดยผู้ใช้ \"{1}\"", + "DownloadClientsLoadError": "ไม่สามารถโหลดไคลเอนต์ดาวน์โหลด", + "UpdateUiNotWritableHealthCheckMessage": "ไม่สามารถติดตั้งการอัปเดตเนื่องจากโฟลเดอร์ UI \"{uiFolder}\" ไม่สามารถเขียนได้โดยผู้ใช้ \"{userName}\"", "Updates": "อัปเดต", "UpdateScriptPathHelpText": "พา ธ ไปยังสคริปต์แบบกำหนดเองที่ใช้แพ็กเกจโปรแกรมปรับปรุงที่แยกออกมาและจัดการส่วนที่เหลือของกระบวนการอัพเดต", "AddDownloadClient": "เพิ่มไคลเอนต์ดาวน์โหลด", @@ -99,9 +99,9 @@ "ScriptPath": "เส้นทางสคริปต์", "Seeders": "Seeders", "SelectAll": "เลือกทั้งหมด", - "SystemTimeCheckMessage": "เวลาของระบบปิดมากกว่า 1 วัน งานที่ตั้งเวลาไว้อาจทำงานไม่ถูกต้องจนกว่าจะมีการแก้ไขเวลา", + "SystemTimeHealthCheckMessage": "เวลาของระบบปิดมากกว่า 1 วัน งานที่ตั้งเวลาไว้อาจทำงานไม่ถูกต้องจนกว่าจะมีการแก้ไขเวลา", "UnableToAddANewNotificationPleaseTryAgain": "ไม่สามารถเพิ่มการแจ้งเตือนใหม่โปรดลองอีกครั้ง", - "UnableToLoadBackups": "ไม่สามารถโหลดข้อมูลสำรอง", + "BackupsLoadError": "ไม่สามารถโหลดข้อมูลสำรอง", "UnableToLoadNotifications": "ไม่สามารถโหลดการแจ้งเตือน", "ApplicationStatusCheckAllClientMessage": "รายการทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", "ApplicationStatusCheckSingleClientMessage": "รายการไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {0}", @@ -139,7 +139,7 @@ "CustomFilters": "ตัวกรองที่กำหนดเอง", "Date": "วันที่", "Dates": "วันที่", - "DBMigration": "การย้ายฐานข้อมูล", + "DatabaseMigration": "การย้ายฐานข้อมูล", "Delete": "ลบ", "DeleteApplicationMessageText": "แน่ใจไหมว่าต้องการลบการแจ้งเตือน \"{0}\"", "DeleteBackup": "ลบข้อมูลสำรอง", @@ -149,8 +149,8 @@ "DownloadClient": "ดาวน์โหลดไคลเอนต์", "DownloadClients": "ดาวน์โหลดไคลเอนต์", "DownloadClientSettings": "ดาวน์โหลด Client Settings", - "DownloadClientStatusCheckAllClientMessage": "ไคลเอนต์ดาวน์โหลดทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลว", - "DownloadClientStatusCheckSingleClientMessage": "ดาวน์โหลดไคลเอ็นต์ไม่ได้เนื่องจากความล้มเหลว: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "ไคลเอนต์ดาวน์โหลดทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลว", + "DownloadClientStatusSingleClientHealthCheckMessage": "ดาวน์โหลดไคลเอ็นต์ไม่ได้เนื่องจากความล้มเหลว: {downloadClientNames}", "Edit": "แก้ไข", "EditIndexer": "แก้ไข Indexer", "Enable": "เปิดใช้งาน", @@ -166,7 +166,7 @@ "Grabbed": "คว้า", "Grabs": "คว้า", "Health": "สุขภาพ", - "HealthNoIssues": "ไม่มีปัญหากับการกำหนดค่าของคุณ", + "NoIssuesWithYourConfiguration": "ไม่มีปัญหากับการกำหนดค่าของคุณ", "HideAdvanced": "ซ่อนขั้นสูง", "History": "ประวัติศาสตร์", "HomePage": "หน้าแรก", @@ -174,12 +174,12 @@ "IllRestartLater": "ฉันจะรีสตาร์ทในภายหลัง", "Indexer": "Indexer", "IndexerFlags": "ดัชนีดัชนี", - "IndexerLongTermStatusCheckAllClientMessage": "ตัวจัดทำดัชนีทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง", - "IndexerLongTermStatusCheckSingleClientMessage": "ดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง: {0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "ตัวจัดทำดัชนีทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "ดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง: {indexerNames}", "IndexerPriority": "ลำดับความสำคัญของ Indexer", "IndexerPriorityHelpText": "ลำดับความสำคัญของดัชนีจาก 1 (สูงสุด) ถึง 50 (ต่ำสุด) ค่าเริ่มต้น: 25.", - "IndexerStatusCheckAllClientMessage": "ตัวทำดัชนีทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", - "IndexerStatusCheckSingleClientMessage": "ตัวจัดทำดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "ตัวทำดัชนีทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", + "IndexerStatusUnavailableHealthCheckMessage": "ตัวจัดทำดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {indexerNames}", "MIA": "MIA", "NoBackupsAreAvailable": "ไม่มีการสำรองข้อมูล", "NoChange": "ไม่มีการเปลี่ยนแปลง", @@ -195,11 +195,11 @@ "Protocol": "มาตรการ", "Proxy": "พร็อกซี", "ProxyBypassFilterHelpText": "ใช้ \",\" เป็นตัวคั่นและ \"*.\" เป็นสัญลักษณ์แทนสำหรับโดเมนย่อย", - "ProxyCheckBadRequestMessage": "ไม่สามารถทดสอบพร็อกซี StatusCode: {0}", + "ProxyBadRequestHealthCheckMessage": "ไม่สามารถทดสอบพร็อกซี StatusCode: {statusCode}", "Restore": "คืนค่า", "RestoreBackup": "คืนค่าการสำรองข้อมูล", "Retention": "การเก็บรักษา", - "RSS": "RSS", + "Rss": "RSS", "SendAnonymousUsageData": "ส่งข้อมูลการใช้งานแบบไม่ระบุตัวตน", "SetTags": "ตั้งแท็ก", "Settings": "การตั้งค่า", @@ -216,8 +216,8 @@ "UnsavedChanges": "การเปลี่ยนแปลงที่ไม่ได้บันทึก", "UnselectAll": "ไม่เลือกทั้งหมด", "UpdateAutomaticallyHelpText": "ดาวน์โหลดและติดตั้งการอัปเดตโดยอัตโนมัติ คุณจะยังติดตั้งได้จาก System: Updates", - "UpdateCheckStartupNotWritableMessage": "ไม่สามารถติดตั้งการอัปเดตเนื่องจากโฟลเดอร์เริ่มต้น \"{0}\" ไม่สามารถเขียนได้โดยผู้ใช้ \"{1}\"", - "UpdateCheckStartupTranslocationMessage": "ไม่สามารถติดตั้งการอัปเดตได้เนื่องจากโฟลเดอร์เริ่มต้น \"{0}\" อยู่ในโฟลเดอร์การแปลแอป", + "UpdateStartupNotWritableHealthCheckMessage": "ไม่สามารถติดตั้งการอัปเดตเนื่องจากโฟลเดอร์เริ่มต้น \"{startupFolder}\" ไม่สามารถเขียนได้โดยผู้ใช้ \"{userName}\"", + "UpdateStartupTranslocationHealthCheckMessage": "ไม่สามารถติดตั้งการอัปเดตได้เนื่องจากโฟลเดอร์เริ่มต้น \"{startupFolder}\" อยู่ในโฟลเดอร์การแปลแอป", "Uptime": "เวลาทำงาน", "URLBase": "ฐาน URL", "UrlBaseHelpText": "สำหรับการสนับสนุน reverse proxy ค่าเริ่มต้นจะว่างเปล่า", @@ -250,7 +250,7 @@ "DeleteNotificationMessageText": "แน่ใจไหมว่าต้องการลบการแจ้งเตือน \"{0}\"", "DeleteTag": "ลบแท็ก", "EnableSSL": "เปิดใช้งาน SSL", - "ProxyCheckResolveIpMessage": "ไม่สามารถแก้ไขที่อยู่ IP สำหรับโฮสต์พร็อกซีที่กำหนดค่าไว้ {0}", + "ProxyResolveIpHealthCheckMessage": "ไม่สามารถแก้ไขที่อยู่ IP สำหรับโฮสต์พร็อกซีที่กำหนดค่าไว้ {proxyHostName}", "ProxyType": "ประเภทพร็อกซี", "Cancel": "ยกเลิก", "NoLogFiles": "ไม่มีไฟล์บันทึก", @@ -274,7 +274,7 @@ "ResetAPIKey": "รีเซ็ตคีย์ API", "RestartNow": "เริ่มต้นใหม่เดี๋ยวนี้", "RestartRequiredHelpTextWarning": "ต้องรีสตาร์ทเพื่อให้มีผล", - "RSSIsNotSupportedWithThisIndexer": "RSS ไม่ได้รับการสนับสนุนกับตัวสร้างดัชนีนี้", + "RssIsNotSupportedWithThisIndexer": "RSS ไม่ได้รับการสนับสนุนกับตัวสร้างดัชนีนี้", "Search": "ค้นหา", "Security": "ความปลอดภัย", "SSLCertPassword": "รหัสผ่านใบรับรอง SSL", @@ -329,7 +329,7 @@ "Queued": "อยู่ในคิว", "Remove": "ลบ", "Replace": "แทนที่", - "TheLatestVersionIsAlreadyInstalled": "มีการติดตั้ง {appName} เวอร์ชันล่าสุดแล้ว", + "OnLatestVersion": "มีการติดตั้ง {appName} เวอร์ชันล่าสุดแล้ว", "Track": "ติดตาม", "DeleteSelectedApplicationsMessageText": "แน่ใจไหมว่าต้องการลบตัวสร้างดัชนี \"{0}\"", "ApplyTagsHelpTextAdd": "เพิ่ม: เพิ่มแท็กในรายการแท็กที่มีอยู่", @@ -350,11 +350,33 @@ "WhatsNew": "มีอะไรใหม่", "RecentChanges": "การเปลี่ยนแปลงล่าสุด", "NotificationStatusAllClientHealthCheckMessage": "รายการทั้งหมดไม่พร้อมใช้งานเนื่องจากความล้มเหลว", - "NotificationStatusSingleClientHealthCheckMessage": "รายการไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "รายการไม่พร้อมใช้งานเนื่องจากความล้มเหลว: {notificationNames}", "AuthBasic": "พื้นฐาน (เบราว์เซอร์ป๊อปอัพ)", "AuthForm": "แบบฟอร์ม (หน้าเข้าสู่ระบบ)", "DisabledForLocalAddresses": "ปิดใช้งานสำหรับที่อยู่ท้องถิ่น", "None": "ไม่มี", "ResetAPIKeyMessageText": "แน่ใจไหมว่าต้องการรีเซ็ตคีย์ API", - "RestartProwlarr": "รีสตาร์ท {appName}" + "RestartProwlarr": "รีสตาร์ท {appName}", + "IndexerHDBitsSettingsMediums": "ปานกลาง", + "CustomFilter": "ตัวกรองที่กำหนดเอง", + "ProxyValidationBadRequest": "ไม่สามารถทดสอบพร็อกซี StatusCode: {statusCode}", + "GrabRelease": "คว้ารีลีส", + "Script": "สคริปต์", + "BuiltIn": "สร้างขึ้นใน", + "PublishedDate": "วันที่เผยแพร่", + "AllSearchResultsHiddenByFilter": "ผลลัพธ์ทั้งหมดถูกซ่อนโดยตัวกรองที่ใช้", + "AptUpdater": "ใช้ apt เพื่อติดตั้งการอัปเดต", + "Download": "ดาวน์โหลด", + "ErrorRestoringBackup": "เกิดข้อผิดพลาดในการกู้คืนข้อมูลสำรอง", + "DockerUpdater": "อัปเดตคอนเทนเนอร์นักเทียบท่าเพื่อรับการอัปเดต", + "ExternalUpdater": "{appName} ถูกกำหนดค่าให้ใช้กลไกการอัพเดตภายนอก", + "NoEventsFound": "ไม่พบกิจกรรม", + "RestartReloadNote": "หมายเหตุ: {appName} จะรีสตาร์ทและโหลด UI ใหม่โดยอัตโนมัติในระหว่างกระบวนการกู้คืน", + "UpdateAppDirectlyLoadError": "ไม่สามารถอัปเดต {appName} ได้โดยตรง", + "InstallLatest": "ติดตั้งล่าสุด", + "Clone": "ปิด", + "Mixed": "แก้ไขแล้ว", + "Stats": "สถานะ", + "CurrentlyInstalled": "ติดตั้งแล้ว", + "Season": "เหตุผล" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index d18c1e40b..0d4af37ac 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -4,58 +4,58 @@ "Dates": "Tarih", "Date": "Tarih", "Connections": "Bağlantılar", - "Connect": "Bağlan", + "Connect": "Bildirimler", "Clear": "Temizle", - "Sort": "Çeşitle", + "Sort": "Sınıflandır", "SetTags": "Etiketleri Ayarla", - "Scheduled": "Tarifeli", - "ProxyCheckResolveIpMessage": "{0} Yapılandırılmış Proxy Ana Bilgisayarının IP Adresi çözülemedi", - "ProxyCheckFailedToTestMessage": "Proxy ile test edilemedi: {0}", - "ProxyCheckBadRequestMessage": "Proxy ile test edilemedi. DurumKodu: {0}", + "Scheduled": "Planlı", + "ProxyResolveIpHealthCheckMessage": "Yapılandırılmış Proxy Ana Bilgisayarı {proxyHostName} için IP Adresi çözümlenemedi", + "ProxyFailedToTestHealthCheckMessage": "Proxy test edilemedi: {url}", + "ProxyBadRequestHealthCheckMessage": "Proxy test edilemedi. Durum kodu: {statusCode}", "Proxy": "Proxy", - "Logging": "Logging", - "LogFiles": "Log dosyaları", - "Host": "Ana bilgisayar", - "GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analitikler ve güncellemeler", + "Logging": "Loglama", + "LogFiles": "Log Kayıtları", + "Host": "Sunucu", + "GeneralSettingsSummary": "Port, SSL, kullanıcı adı/şifre, proxy, analiz ve güncellemeler", "Folder": "Klasör", "Files": "Dosyalar", "Filename": "Dosya adı", - "AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak", - "Actions": "Etkiler", + "AppDataLocationHealthCheckMessage": "Güncelleme sırasında AppData'nın silinmesini önlemek için güncelleme yapılmayacaktır", + "Actions": "Eylemler", "About": "Hakkında", "View": "Görünüm", "Updates": "Güncellemeler", - "UpdateCheckUINotWritableMessage": "'{0}' UI klasörü '{1}' kullanıcısı tarafından yazılamadığından güncelleme yüklenemiyor.", - "UpdateCheckStartupTranslocationMessage": "Başlangıç klasörü '{0}' bir Uygulama Yer Değiştirme klasöründe olduğu için güncelleme yüklenemiyor.", - "UpdateCheckStartupNotWritableMessage": "'{0}' başlangıç klasörü '{1}' kullanıcısı tarafından yazılamadığından güncelleme yüklenemiyor.", + "UpdateUiNotWritableHealthCheckMessage": "UI klasörü '{uiFolder}' '{userName}' kullanıcısı tarafından yazılabilir olmadığından güncelleme yüklenemiyor.", + "UpdateStartupTranslocationHealthCheckMessage": "Başlangıç klasörü '{startupFolder}' bir Uygulama Taşıma klasöründe olduğundan güncelleme yüklenemiyor.", + "UpdateStartupNotWritableHealthCheckMessage": "Başlangıç klasörü '{startupFolder}' '{userName}' kullanıcısı tarafından yazılabilir olmadığından güncelleme yüklenemiyor.", "UnselectAll": "Tüm Seçimleri Kaldır", - "UISettingsSummary": "Takvim, tarih ve renk engelli seçenekler", + "UISettingsSummary": "Takvim, tarih ve renk engelli seçenekleri", "UI": "UI", "Tasks": "Görevler", "TagsSettingsSummary": "Tüm etiketleri ve nasıl kullanıldıklarını göster. Kullanılmayan etiketler kaldırılabilinir", "Tags": "Etiketler", "System": "Sistem", - "Style": "Tarz", + "Style": "Stil", "Status": "Durum", - "Size": "Ölçü", + "Size": "Boyut", "ShowAdvanced": "Gelişmiş'i Göster", "Settings": "Ayarlar", - "SelectAll": "Hepsini seç", + "SelectAll": "Hepsini Seç", "Security": "Güvenlik", "Search": "Ara", "SaveChanges": "Değişiklikleri Kaydet", "ReleaseStatus": "Yayın Durumu", - "ReleaseBranchCheckOfficialBranchMessage": "Dal {0} geçerli bir {appName} sürüm dalı değil, güncelleme almayacaksınız", + "ReleaseBranchCheckOfficialBranchMessage": "{0} şubesi geçerli bir {appName} sürüm dalı değil; güncelleme almayacaksınız", "Refresh": "Yenile", - "Queue": "Sıra", + "Queue": "Kuyruk", "Protocol": "Protokol", "Options": "Seçenekler", "NoChanges": "Değişiklikler yok", "NoChange": "Değişiklik yok", - "MoreInfo": "Daha fazla bilgi", + "MoreInfo": "Daha Fazla Bilgi", "LastWriteTime": "Son Yazma Zamanı", "Language": "Dil", - "History": "Tarih", + "History": "Geçmiş", "HideAdvanced": "Gelişmiş'i Gizle", "Health": "Sağlık", "General": "Genel", @@ -63,100 +63,100 @@ "Failed": "Başarısız oldu", "Edit": "Düzenle", "CustomFilters": "Özel Filtreler", - "ConnectSettingsSummary": "Bildirimler, medya sunucularına/oynatıcılara bağlantılar ve özel komut kodları", - "Analytics": "Analitik", - "All": "Herşey", - "Added": "Eklendi", + "ConnectSettingsSummary": "Bildirimler ve özel komut dosyaları", + "Analytics": "Analiz", + "All": "Hepsi", + "Added": "Eklenme", "Add": "Ekle", "Branch": "Şube", "TestAllClients": "Tüm İstemcileri Test Et", "ErrorLoadingContents": "İçerik yüklenirken hata oluştu", "FeatureRequests": "Özellik talepleri", - "LogLevelTraceHelpTextWarning": "İzleme günlük kaydı yalnızca geçici olarak etkinleştirilmelidir", - "Peers": "Akranlar", + "LogLevelTraceHelpTextWarning": "İzleme kaydı yalnızca geçici olarak etkinleştirilmelidir", + "Peers": "Eşler", "Presets": "Ön ayarlar", "RemoveFilter": "Filtreyi kaldır", "SettingsEnableColorImpairedMode": "Renk Bozukluğu Modunu Etkinleştir", "ShowSearchHelpText": "Fareyle üzerine gelindiğinde arama düğmesini göster", "Shutdown": "Kapat", - "TableOptions": "Masa Seçenekleri", + "TableOptions": "Tablo Seçenekleri", "UnableToLoadTags": "Etiketler yüklenemiyor", "UnsavedChanges": "Kaydedilmemiş Değişiklikler", "Backups": "Yedeklemeler", - "BindAddress": "Bağlama Adresi", - "BypassProxyForLocalAddresses": "Yerel Adresler için Proxy'yi Atla", - "DeleteNotificationMessageText": "'{0}' bildirimini silmek istediğinizden emin misiniz?", + "BindAddress": "Bind Adresi", + "BypassProxyForLocalAddresses": "Yerel Adresler için Proxy'yi Kullanma", + "DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?", "EnableSslHelpText": " Etkili olması için yönetici olarak yeniden çalıştırmayı gerektirir", - "Fixed": "Sabit", + "Fixed": "Düzeltilen", "PendingChangesMessage": "Kaydedilmemiş değişiklikleriniz var, bu sayfadan ayrılmak istediğinizden emin misiniz?", "PendingChangesStayReview": "Kalın ve değişiklikleri inceleyin", - "Port": "Liman", + "Port": "Port No", "PortNumber": "Port numarası", "RestoreBackup": "Yedeği Geri Yükle", - "RSS": "RSS", - "Save": "Kayıt etmek", + "Rss": "RSS", + "Save": "Kaydet", "SaveSettings": "Ayarları kaydet", "ScriptPath": "Komut Dosyası Yolu", - "Test": "Ölçek", + "Test": "Test Et", "TestAll": "Tümünü Test Et", "UnableToAddANewApplicationPleaseTryAgain": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", "YesCancel": "Evet İptal", - "ApplicationStatusCheckAllClientMessage": "Hatalar nedeniyle tüm listeler kullanılamıyor", - "CancelPendingTask": "Bu bekleyen görevi iptal etmek istediğinizden emin misiniz?", + "ApplicationStatusCheckAllClientMessage": "Hatalar nedeniyle tüm uygulamalar kullanılamıyor", + "CancelPendingTask": "Bekleyen görevi iptal etmek istediğinizden emin misiniz?", "DeleteTag": "Etiketi Sil", - "BindAddressHelpText": "Tüm arayüzler için geçerli IP4 adresi veya '*'", + "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", "ConnectSettings": "Bağlantı Ayarları", - "DBMigration": "DB Geçişi", - "DeleteApplicationMessageText": "'{0}' bildirimini silmek istediğinizden emin misiniz?", + "DatabaseMigration": "DB Geçişi", + "DeleteApplicationMessageText": "'{name}' uygulamasını silmek istediğinizden emin misiniz?", "DeleteBackup": "Yedeklemeyi Sil", - "DeleteBackupMessageText": "'{0}' yedeğini silmek istediğinizden emin misiniz?", + "DeleteBackupMessageText": "'{name}' yedeğini silmek istediğinizden emin misiniz?", "DeleteDownloadClient": "İndirme İstemcisini Sil", - "DeleteDownloadClientMessageText": "İndirme istemcisini '{0}' silmek istediğinizden emin misiniz?", - "DownloadClientSettings": "İstemci Ayarlarını İndir", + "DeleteDownloadClientMessageText": "'{name}' indirme istemcisini silmek istediğinizden emin misiniz?", + "DownloadClientSettings": "İndirme İstemcisi Ayarlarını", "EnableAutomaticSearchHelpText": "Kullanıcı arayüzü veya {appName} tarafından otomatik aramalar yapıldığında kullanılacaktır", "ForMoreInformationOnTheIndividualDownloadClients": "Bireysel indirme istemcileri hakkında daha fazla bilgi için bilgi düğmelerine tıklayın.", "Hostname": "Hostname", "OpenThisModal": "Bu Modeli Aç", "Manual": "Manuel", - "Mechanism": "Mekanizma", - "Message": "İleti", + "Mechanism": "Teknik", + "Message": "Mesaj", "MIA": "MIA", "MovieIndexScrollBottom": "Film Dizini: Alta Kaydırma", "MovieIndexScrollTop": "Film Dizini: Yukarı Kaydırma", "NoLinks": "Bağlantı Yok", - "PackageVersion": "Paket Sürümü", - "PageSize": "Sayfa boyutu", + "PackageVersion": "Paket Versiyonu", + "PageSize": "Sayfa Boyutu", "PageSizeHelpText": "Her sayfada gösterilecek öğe sayısı", "ProxyBypassFilterHelpText": "Ayırıcı olarak \",\" ve \"*\" kullanın. alt alan adları için joker karakter olarak", "ProxyUsernameHelpText": "Gerekirse yalnızca bir kullanıcı adı ve şifre girmeniz gerekir. Aksi takdirde boş bırakın.", "Reload": "Tekrar yükle", "RemovedFromTaskQueue": "Görev kuyruğundan kaldırıldı", "SendAnonymousUsageData": "Anonim Kullanım Verilerini Gönderin", - "Age": "Yaş", - "AllIndexersHiddenDueToFilter": "Uygulanan filtre nedeniyle tüm filmler gizlendi.", - "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} WebUI sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız.", + "Age": "Yıl", + "AllIndexersHiddenDueToFilter": "Uygulanan filtre nedeniyle tüm indeksleyiciler gizlendi.", + "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.", "ApiKey": "API Anahtarı", - "AppDataDirectory": "AppData dizini", - "NoUpdatesAreAvailable": "Güncelleme yok", + "AppDataDirectory": "Uygulama Veri Dizini", + "NoUpdatesAreAvailable": "Güncelleme bulunamadı", "OAuthPopupMessage": "Pop-up'lar tarayıcınız tarafından engelleniyor", "Ok": "Tamam", "OnHealthIssueHelpText": "Sağlık Sorunu Hakkında", - "BranchUpdate": "{appName}'ı güncellemek için kullanılacak dal", + "BranchUpdate": "{appName} uygulamasını güncellemek için kullanılacak şube", "Close": "Kapat", - "ApplicationStatusCheckSingleClientMessage": "Hatalar nedeniyle kullanılamayan listeler: {0}", + "ApplicationStatusCheckSingleClientMessage": "Hatalar nedeniyle kullanılamayan uygulamalar: {0}", "ApplyTags": "Etiketleri Uygula", - "RSSIsNotSupportedWithThisIndexer": "RSS, bu indeksleyici ile desteklenmiyor", - "Interval": "Aralık", - "Logs": "Kütükler", + "RssIsNotSupportedWithThisIndexer": "RSS, bu indeksleyici ile desteklenmiyor", + "Interval": "Periyot", + "Logs": "Kayıtlar", "Authentication": "Doğrulama", - "AuthenticationMethodHelpText": "{appName}'a erişmek için Kullanıcı Adı ve Şifre gerektir", - "BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki aralık", - "BackupNow": "Şimdi yedekle", - "BackupRetentionHelpText": "Saklama süresinden daha eski olan otomatik yedeklemeler otomatik olarak temizlenecektir", + "AuthenticationMethodHelpText": "{appName}'e erişmek için Kullanıcı Adı ve Parola gereklidir", + "BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki zaman aralığı", + "BackupNow": "Şimdi Yedekle", + "BackupRetentionHelpText": "Saklama süresinden daha eski otomatik yedeklemeler otomatik olarak temizlenecektir", "BeforeUpdate": "Güncellemeden önce", - "BranchUpdateMechanism": "Harici güncelleme mekanizması tarafından kullanılan dal", - "CertificateValidation": "Sertifika Doğrulama", - "CertificateValidationHelpText": "HTTPS sertifika doğrulamasının ne kadar katı olduğunu değiştirin", + "BranchUpdateMechanism": "Harici güncelleme mekanizması tarafından kullanılan şube", + "CertificateValidation": "Sertifika Doğrulaması", + "CertificateValidationHelpText": "HTTPS sertifika doğrulamasının sıkılığını değiştir", "ChangeHasNotBeenSavedYet": "Değişiklik henüz kaydedilmedi", "ClientPriority": "Müşteri Önceliği", "CloneProfile": "Klon Profili", @@ -164,42 +164,42 @@ "Columns": "Sütunlar", "Component": "Bileşen", "ConnectionLost": "Bağlantı koptu", - "DeleteIndexerProxyMessageText": "'{0}' etiketini silmek istediğinizden emin misiniz?", + "DeleteIndexerProxyMessageText": "'{name}' indeksleyici proxy'sini silmek istediğinizden emin misiniz?", "DeleteNotification": "Bildirimi Sil", - "DeleteTagMessageText": "'{0}' etiketini silmek istediğinizden emin misiniz?", + "DeleteTagMessageText": "'{label}' etiketini silmek istediğinizden emin misiniz?", "Disabled": "Devre dışı", - "Discord": "Uyuşmazlık", - "Docker": "Liman işçisi", - "Donations": "Bağışlar", - "DownloadClient": "İstemciyi İndir", - "DownloadClients": "İstemcileri İndir", + "Discord": "Discord", + "Docker": "Docker", + "Donations": "Bağış", + "DownloadClient": "İndirme İstemcisi", + "DownloadClients": "İndirme İstemcileri", "EnableAutomaticSearch": "Otomatik Aramayı Etkinleştir", "EnableInteractiveSearchHelpText": "Etkileşimli arama kullanıldığında kullanılacak", "FocusSearchBox": "Arama Kutusuna Odaklan", "GeneralSettings": "Genel Ayarlar", - "Grabs": "Kapmak", - "HealthNoIssues": "Yapılandırmanızla ilgili sorun yok", + "Grabs": "İndirenler", + "NoIssuesWithYourConfiguration": "Yapılandırmanızla ilgili sorun yok", "HomePage": "Ana Sayfa", "IllRestartLater": "Daha sonra yeniden başlayacağım", "IncludeHealthWarningsHelpText": "Sağlık Uyarılarını Dahil Et", - "IndexerFlags": "Dizin Oluşturucu Bayrakları", - "IndexerLongTermStatusCheckAllClientMessage": "6 saatten uzun süren arızalar nedeniyle tüm dizinleyiciler kullanılamıyor", - "IndexerLongTermStatusCheckSingleClientMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle dizinleyiciler kullanılamıyor: {0}", - "IndexerPriority": "Dizin Oluşturucu Önceliği", - "IndexerPriorityHelpText": "1 (En Yüksek) ila 50 (En Düşük) arasında Dizin Oluşturucu Önceliği. Varsayılan: 25.", - "IndexerStatusCheckAllClientMessage": "Hatalar nedeniyle tüm dizinleyiciler kullanılamıyor", - "IndexerStatusCheckSingleClientMessage": "Hatalar nedeniyle dizinleyiciler kullanılamıyor: {0}", + "IndexerFlags": "İndeksleyici Bayrakları", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle tüm indeksleyiciler kullanılamıyor", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "6 saatten uzun süren hatalar nedeniyle kullanılamayan indeksleyiciler: {indexerNames}", + "IndexerPriority": "İndeksleyici Önceliği", + "IndexerPriorityHelpText": "İndeksleyici Önceliği 1 (En Yüksek) ile 50 (En Düşük) arasında. Varsayılan: 25.", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tüm indeksleyiciler hatalar nedeniyle kullanılamıyor", + "IndexerStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle kullanılamayan indeksleyiciler: {indexerNames}", "Info": "Bilgi", "InteractiveSearch": "Etkileşimli Arama", "KeyboardShortcuts": "Klavye kısayolları", "LaunchBrowserHelpText": " Bir web tarayıcısı açın ve uygulama başlangıcında {appName} ana sayfasına gidin.", - "LogLevel": "Günlük Düzeyi", + "LogLevel": "Log Seviyesi", "Mode": "Mod", "NoTagsHaveBeenAddedYet": "Henüz etiket eklenmedi", "NotificationTriggers": "Bildirim Tetikleyicileri", "OpenBrowserOnStart": "Başlangıçta tarayıcıyı aç", - "Password": "Parola", - "PendingChangesDiscardChanges": "Değişiklikleri atın ve ayrıl", + "Password": "Şifre", + "PendingChangesDiscardChanges": "Değişiklikleri at ve ayrıl", "Priority": "Öncelik", "ProxyType": "Proxy Türü", "ReadTheWikiForMoreInformation": "Daha fazla bilgi için Wiki'yi okuyun", @@ -211,7 +211,7 @@ "RestartNow": "Şimdi yeniden başlat", "Result": "Sonuç", "Retention": "Saklama", - "SettingsEnableColorImpairedModeHelpText": "Renk bozukluğu olan kullanıcıların renk kodlu bilgileri daha iyi ayırt etmesine olanak tanıyan değiştirilmiş stil", + "SettingsEnableColorImpairedModeHelpText": "Renk engelli kullanıcıların renkleri daha iyi ayırt etmesine olanak tanıyan değiştirilmiş stil", "SettingsLongDateFormat": "Uzun Tarih Formatı", "SettingsShortDateFormat": "Kısa Tarih Formatı", "SettingsShowRelativeDates": "Göreli Tarihleri Göster", @@ -224,10 +224,10 @@ "SSLCertPathHelpText": "Pfx dosyasının yolu", "SSLPort": "SSL Bağlantı Noktası", "StartTypingOrSelectAPathBelow": "Yazmaya başlayın veya aşağıdan bir yol seçin", - "StartupDirectory": "Başlangıç dizini", + "StartupDirectory": "Başlangıç Dizini", "SuggestTranslationChange": "Çeviri değişikliği önerin", - "SystemTimeCheckMessage": "Sistem saati 1 günden fazla kapalı. Zamanlanan görevler, saat düzeltilene kadar doğru çalışmayabilir", - "TableOptionsColumnsMessage": "Hangi sütunların görünür olduğunu ve hangi sırada görüneceklerini seçin", + "SystemTimeHealthCheckMessage": "Sistem saati 1 günden fazla kapalı. Zamanlanan görevler, saat düzeltilene kadar doğru çalışmayabilir", + "TableOptionsColumnsMessage": "Hangi sütunların görünür olacağını ve hangi sırayla görüneceğini seçin", "Title": "Başlık", "Today": "Bugün", "Tomorrow": "Yarın", @@ -239,53 +239,53 @@ "UISettings": "UI Ayarları", "UnableToAddANewAppProfilePleaseTryAgain": "Yeni bir kaliteli profil eklenemiyor, lütfen tekrar deneyin.", "UnableToAddANewDownloadClientPleaseTryAgain": "Yeni bir indirme istemcisi eklenemiyor, lütfen tekrar deneyin.", - "UnableToAddANewIndexerPleaseTryAgain": "Yeni bir dizinleyici eklenemiyor, lütfen tekrar deneyin.", - "UnableToAddANewIndexerProxyPleaseTryAgain": "Yeni bir dizinleyici eklenemiyor, lütfen tekrar deneyin.", + "UnableToAddANewIndexerPleaseTryAgain": "Yeni bir indeksleyici eklenemiyor, lütfen tekrar deneyin.", + "UnableToAddANewIndexerProxyPleaseTryAgain": "Yeni bir indeksleyici eklenemiyor, lütfen tekrar deneyin.", "UnableToAddANewNotificationPleaseTryAgain": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.", - "UnableToLoadBackups": "Yedeklemeler yüklenemiyor", + "BackupsLoadError": "Yedeklemeler yüklenemiyor", "UnableToLoadHistory": "Geçmiş yüklenemiyor", "UnableToLoadNotifications": "Bildirimler yüklenemiyor", "UnableToLoadUISettings": "UI ayarları yüklenemiyor", "Yesterday": "Dün", - "AcceptConfirmationModal": "Onay Modelini Kabul Et", - "AddIndexer": "Dizin Oluşturucu Ekle", + "AcceptConfirmationModal": "Onay Modunu Kabul Et", + "AddIndexer": "İndeksleyici Ekle", "AddDownloadClient": "İndirme İstemcisi Ekle", "AddingTag": "Etiket ekleniyor", "CouldNotConnectSignalR": "SignalR'ye bağlanılamadı, kullanıcı arayüzü güncellenmeyecek", "Custom": "Özel", - "DownloadClientStatusCheckSingleClientMessage": "Hatalar nedeniyle indirilemeyen istemciler: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Hatalar nedeniyle indirme istemcileri kullanılamıyor: {downloadClientNames}", "Enabled": "Etkin", "IgnoredAddresses": "Yoksayılan Adresler", - "Indexer": "Dizin oluşturucu", - "DownloadClientStatusCheckAllClientMessage": "Hatalar nedeniyle tüm indirme istemcileri kullanılamıyor", - "EditIndexer": "Dizinleyiciyi Düzenle", - "Enable": "etkinleştirme", + "Indexer": "İndeksleyici", + "DownloadClientStatusAllClientHealthCheckMessage": "Tüm indirme istemcileri hatalar nedeniyle kullanılamıyor", + "EditIndexer": "İndeksleyiciyi Düzenle", + "Enable": "Etkinleştir", "EnableInteractiveSearch": "Etkileşimli Aramayı Etkinleştir", "EnableRss": "RSS'yi etkinleştir", "EnableSSL": "SSL'yi etkinleştir", "NoLeaveIt": "Hayır, Bırak", "Error": "Hata", - "Events": "Etkinlikler", + "Events": "Olaylar", "EventType": "Etkinlik tipi", "Exception": "İstisna", "ExistingTag": "Mevcut etiket", - "IndexerProxyStatusCheckAllClientMessage": "Hatalar nedeniyle tüm dizinleyiciler kullanılamıyor", - "IndexerProxyStatusCheckSingleClientMessage": "Hatalar nedeniyle dizinleyiciler kullanılamıyor: {0}", - "Indexers": "Dizin oluşturucular", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Hatalar nedeniyle tüm indeksleyiciler kullanılamıyor", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Hatalar nedeniyle indeksleyiciler kullanılamıyor: {indexerProxyNames}", + "Indexers": "İndeksleyiciler", "Name": "İsim", "New": "Yeni", "NoBackupsAreAvailable": "Kullanılabilir yedek yok", - "NoLogFiles": "Günlük dosyası yok", + "NoLogFiles": "Log kayıt dosyası henüz oluşturulmadı", "Restart": "Tekrar başlat", "RestartRequiredHelpTextWarning": "Etkili olması için yeniden başlatma gerektirir", "Restore": "Onarmak", "Seeders": "Ekme makineleri", "TagCannotBeDeletedWhileInUse": "Kullanımdayken silinemez", "TagIsNotUsedAndCanBeDeleted": "Etiket kullanılmaz ve silinebilir", - "TagsHelpText": "En az bir eşleşen etikete sahip filmler için geçerlidir", + "TagsHelpText": "En az bir eşleşen etiketi olan indeksleyiciler için geçerlidir", "UILanguage": "UI Dili", "UpdateScriptPathHelpText": "Çıkarılan bir güncelleme paketini alan ve güncelleme işleminin geri kalanını işleyen özel bir komut dosyasına giden yol", - "Uptime": "Uptime", + "Uptime": "Çalışma süresi", "URLBase": "URL Tabanı", "UrlBaseHelpText": "Ters proxy desteği için varsayılan boştur", "Usenet": "Usenet", @@ -294,78 +294,518 @@ "Version": "Sürüm", "Warn": "Uyar", "Wiki": "Wiki", - "Apply": "Uygulamak", - "BackupFolderHelpText": "Göreli yollar {appName}'ın AppData dizini altında olacaktır", - "Grabbed": "Yakalandı", + "Apply": "Uygula", + "BackupFolderHelpText": "Bağıl yollar {appName}'ın AppData dizini altında olacak", + "Grabbed": "Alındı", "ProxyPasswordHelpText": "Gerekirse yalnızca bir kullanıcı adı ve şifre girmeniz gerekir. Aksi takdirde boş bırakın.", - "UpdateAutomaticallyHelpText": "Güncellemeleri otomatik olarak indirin ve yükleyin. Yine de Sistem'den yükleyebileceksiniz: Güncellemeler", + "UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz", "UpdateMechanismHelpText": "{appName}'ın yerleşik güncelleyicisini veya bir komut dosyasını kullanın", "ShowSearch": "Aramayı Göster", - "UnableToLoadDownloadClients": "İndirme istemcileri yüklenemiyor", + "DownloadClientsLoadError": "İndirme istemcileri yüklenemiyor", "UnableToLoadGeneralSettings": "Genel ayarlar yüklenemiyor", "Automatic": "Otomatik", "AutomaticSearch": "Otomatik Arama", - "Backup": "Destek olmak", - "Cancel": "İptal etmek", + "Backup": "Yedekler", + "Cancel": "Vazgeç", "Level": "Seviye", "Time": "Zaman", "MaintenanceRelease": "Bakım Sürümü: hata düzeltmeleri ve diğer iyileştirmeler. Daha fazla ayrıntı için Github İşlem Geçmişine bakın", "HistoryCleanupDaysHelpText": "Otomatik temizlemeyi devre dışı bırakmak için 0'a ayarlayın", "HistoryCleanupDaysHelpTextWarning": "Geri dönüşüm kutusundaki, seçilen gün sayısından daha eski olan dosyalar otomatik olarak temizlenecektir", - "Filters": "Filtre", - "OnGrab": "Yakalandığında", + "Filters": "Filtreler", + "OnGrab": "Yayın Alındığında", "OnHealthIssue": "Sağlık Sorunu Hakkında", - "TestAllIndexers": "Tüm Dizinleyicileri Test Et", - "GrabReleases": "Bırakma", + "TestAllIndexers": "İndeksleyicileri Test Et", + "GrabReleases": "Sürümü Al", "No": "Hayır", - "NetCore": ".NET Çekirdeği", - "UnableToLoadIndexers": "Dizinleyiciler yüklenemiyor", + "NetCore": ".NET", + "UnableToLoadIndexers": "İndeksleyiciler yüklenemiyor", "Yes": "Evet", "Link": "Bağlantılar", - "MappedDrivesRunningAsService": "Eşlenen ağ sürücüleri, bir Windows Hizmeti olarak çalışırken kullanılamaz. Daha fazla bilgi için lütfen SSS bölümüne bakın", + "MappedDrivesRunningAsService": "Windows Hizmeti olarak çalıştırıldığında eşlenen ağ sürücüleri kullanılamaz. Daha fazla bilgi için lütfen SSS'ye bakın", "Ended": "Bitti", - "LastDuration": "lastDuration", + "LastDuration": "Son Süre", "LastExecution": "Son Yürütme", "NextExecution": "Sonraki Yürütme", - "Queued": "Sıraya alındı", - "ApplicationLongTermStatusCheckAllClientMessage": "6 saatten uzun süren arızalar nedeniyle tüm dizinleyiciler kullanılamıyor", - "ApplicationLongTermStatusCheckSingleClientMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle dizinleyiciler kullanılamıyor: {0}", - "Remove": "Kaldırmak", - "Replace": "Değiştir", - "TheLatestVersionIsAlreadyInstalled": "{appName}'ın en son sürümü zaten kurulu", - "ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin", - "ApplyTagsHelpTextHowToApplyApplications": "Seçilen filmlere etiketler nasıl uygulanır", - "ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldırın", - "ApplyTagsHelpTextHowToApplyIndexers": "Seçilen filmlere etiketler nasıl uygulanır", - "ApplyTagsHelpTextReplace": "Değiştir: Etiketleri girilen etiketlerle değiştirin (tüm etiketleri temizlemek için hiçbir etiket girmeyin)", - "DeleteSelectedDownloadClients": "İndirme İstemcisini Sil", - "DownloadClientPriorityHelpText": "Birden çok İndirme İstemcisine öncelik verin. Round-Robin, aynı önceliğe sahip müşteriler için kullanılır.", - "Genre": "Türler", + "Queued": "Kuyrukta", + "ApplicationLongTermStatusCheckAllClientMessage": "6 saatten uzun süren arızalar nedeniyle tüm indeksleyiciler kullanılamıyor", + "ApplicationLongTermStatusCheckSingleClientMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle indeksleyiciler kullanılamıyor: {0}", + "Remove": "Kaldır", + "Replace": "Yer Değiştir", + "OnLatestVersion": "{appName}'ın en son sürümü kurulu", + "ApplyTagsHelpTextAdd": "Ekle: Mevcut etiket listesine etiketleri ekleyin", + "ApplyTagsHelpTextHowToApplyApplications": "Seçili uygulamalara etiketler nasıl uygulanır?", + "ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldır", + "ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır", + "ApplyTagsHelpTextReplace": "Değiştir: Etiketleri girilen değerlerde değiştirin (tüm etiketleri kaldırmak için etiket girmeyin)", + "DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil", + "DownloadClientPriorityHelpText": "Birden fazla İndirme İstemcisine öncelik verin. Aynı önceliğe sahip istemciler için Round-Robin algoritması kullanılır.", + "Genre": "Tür", "Track": "İzleme", "Year": "Yıl", "More": "Daha", - "DeleteAppProfileMessageText": "Kalite profilini silmek istediğinizden emin misiniz {0}", - "RecentChanges": "Son değişiklikler", + "DeleteAppProfileMessageText": "'{name}' uygulama profilini silmek istediğinizden emin misiniz?", + "RecentChanges": "Son Değişiklikler", "minutes": "Dakika", - "WhatsNew": "Ne var ne yok?", - "ConnectionLostReconnect": "Radarr otomatik olarak bağlanmayı deneyecek veya aşağıdan yeniden yükle'yi tıklayabilirsiniz.", - "NotificationStatusAllClientHealthCheckMessage": "Hatalar nedeniyle tüm listeler kullanılamıyor", - "NotificationStatusSingleClientHealthCheckMessage": "Hatalar nedeniyle kullanılamayan listeler: {0}", + "WhatsNew": "Neler Yeni?", + "ConnectionLostReconnect": "{appName} otomatik olarak bağlanmayı deneyecek veya aşağıdaki yeniden yükle butonuna tıklayabilirsiniz.", + "NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor", + "NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}", "Applications": "Uygulamalar", "AuthBasic": "Temel (Tarayıcı Açılır Penceresi)", - "AuthForm": "Formlar (Giriş Sayfası)", - "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", + "AuthForm": "Form (Giriş Sayfası)", + "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı", "None": "Yok", "ResetAPIKeyMessageText": "API Anahtarınızı sıfırlamak istediğinizden emin misiniz?", "Categories": "Kategoriler", - "Application": "Uygulamalar", - "Episode": "bölüm", - "AddApplicationImplementation": "Koşul Ekle - {uygulama Adı}", + "Application": "Uygulama", + "Episode": "Bölüm", "AddConnection": "Bağlantı Ekle", - "AddConnectionImplementation": "Koşul Ekle - {uygulama Adı}", - "AddIndexerImplementation": "Koşul Ekle - {uygulama Adı}", - "AddIndexerProxyImplementation": "Koşul Ekle - {uygulama Adı}", - "EditConnectionImplementation": "Koşul Ekle - {uygulama Adı}", - "EditApplicationImplementation": "Koşul Ekle - {uygulama Adı}", - "EditIndexerImplementation": "Koşul Ekle - {uygulama Adı}" + "AddApplicationImplementation": "Uygulama Ekle - {implementationName}", + "AddIndexerImplementation": "İndeksleyici Ekle - {implementationName}", + "AddIndexerProxyImplementation": "İndeksleyici Proxy'sini Ekle - {implementationName}", + "EditConnectionImplementation": "Bildirimi Düzenle - {implementationName}", + "AddConnectionImplementation": "Bağlantı Ekle - {implementationName}", + "RestartProwlarr": "{appName}'ı Yeniden Başlat", + "EditApplicationImplementation": "Uygulamayı Düzenle - {implementationName}", + "EditIndexerImplementation": "İndeksleyiciyi Düzenle - {implementationName}", + "EditIndexerProxyImplementation": "İndeksleyici Proxy'sini Düzenle - {implementationName}", + "AddCustomFilter": "Özel Filtre Ekleyin", + "AddDownloadClientImplementation": "İndirme İstemcisi Ekle - {implementationName}", + "EditDownloadClientImplementation": "İndirme İstemcisini Düzenle - {implementationName}", + "ApplicationURL": "Uygulama URL'si", + "AuthenticationRequired": "Kimlik Doğrulama", + "ApplyChanges": "Değişiklikleri Uygula", + "CountDownloadClientsSelected": "{count} indirme istemcisi seçildi", + "CountIndexersSelected": "{count} indeksleyici seçildi", + "AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.", + "Clone": "Klon", + "Category": "Kategori", + "AppUpdated": "{appName} Güncellendi", + "AppUpdatedVersion": "{appName}, `{version}` sürümüne güncellendi; en son değişikliklerin etkin olabilmesi için {appName} uygulamasını yeniden başlatmanız gereklidir", + "ApplicationUrlHelpText": "Bu uygulamanın http(s)://, bağlantı noktası ve URL tabanını içeren harici URL'si", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Yeni şifreyi onayla", + "AuthenticationRequiredPasswordHelpTextWarning": "Yeni şifre girin", + "AuthenticationRequiredUsernameHelpTextWarning": "Yeni kullanıcı adınızı girin", + "ConnectionLostToBackend": "{appName}'ın arka uçla bağlantısı kesildi ve işlevselliğin geri kazanılması için yeniden yüklenmesi gerekecek.", + "BlackholeFolderHelpText": "{appName} uygulamasının {extension} dosyasını depolayacağı klasör", + "AuthenticationMethod": "Kimlik Doğrulama Yöntemi", + "AuthenticationMethodHelpTextWarning": "Lütfen geçerli bir kimlik doğrulama yöntemi seçin", + "AuthenticationRequiredHelpText": "İstekler için Kimlik doğrulamanın gereklilik ayarını değiştirin. Riskleri anlamadığınız sürece değiştirmeyin.", + "CustomFilter": "Özel Filtre", + "AddRemoveOnly": "Sadece Ekle ve Kaldır", + "AddSyncProfile": "Eşitleme Profili Ekle", + "AddDownloadClientToProwlarr": "İndirme istemcisi eklemek, görsel arayüz üzerinde manuel arama yaparak indirilecek içeriği {appName} uygulamasına direkt olarak eklemenize olanak sağlar.", + "AddApplication": "Uygulama Ekle", + "AddCategory": "Kategori Ekle", + "AddNewIndexer": "Yeni İndekleyici Ekle", + "ActiveApps": "Aktif Uygulamalar", + "ActiveIndexers": "Aktif İndeksleyiciler", + "AdvancedSettingsHiddenClickToShow": "Gelimiş ayarlar gizli, göstermek için tıklayın", + "AddIndexerProxy": "İndeksleyici Proxy'sini Ekle", + "AddedToDownloadClient": "İçerik istemciye eklendi", + "Album": "Albüm", + "AdvancedSettingsShownClickToHide": "Gelişmiş ayarlar gösteriliyor, gizlemek için tıklayın", + "AddToDownloadClient": "İçeriği indirme istemcisine ekle", + "DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın", + "Donate": "Bağış yap", + "Destination": "Hedef", + "Directory": "Dizin", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", + "DownloadClientFloodSettingsAdditionalTags": "Ek Etiketler", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Medyanın özelliklerini etiket olarak ekler. İpuçları örnektir.", + "DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Flood API'sine {url} gibi bir önek ekler", + "Database": "Veri tabanı", + "DefaultNameCopiedProfile": "{name} - Kopyala", + "DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?", + "DeleteSelectedIndexersMessageText": "Seçilen {count} indeksleyiciyi silmek istediğinizden emin misiniz?", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter sayısı kadar güncelleyiniz. Bunu ayarlar veya yapılandırma dosyası üzerinden yapabilirsiniz", + "AppProfileInUse": "Kullanımda Olan Uygulama Profili", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Bir torrent, hash ile engellenirse bazı indeksleyiciler için RSS/Arama sırasında düzgün bir şekilde reddedilemeyebilir; bu özelliğin etkinleştirilmesi, torrent yakalandıktan sonra ancak istemciye gönderilmeden önce reddedilmesine olanak tanır..", + "AppProfileSelectHelpText": "Uygulama profilleri, Uygulama eşitlemede RSS, Otomatik Arama ve İnteraktif Arama ayarlarını kontrol etmek için kullanılır", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "Alma Sırasında Engellenen Torrent Karmalarını Eşitle ve Reddet", + "AppSettingsSummary": "{appName} uygulamasının PVR programlarınızla nasıl etkileşime gireceğini yapılandırmak için uygulamalar ve ayarlar", + "DownloadClientFreeboxSettingsApiUrl": "API URL'si", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox API temel URL'sini API sürümüyle tanımlayın, örneğin '{url}', varsayılan olarak '{defaultApiUrl}' olur", + "DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama token'ı (ör. 'app_token')", + "DownloadClientFreeboxSettingsAppId": "Uygulama kimliği", + "DownloadClientFreeboxSettingsHostHelpText": "Freebox'un istemci adı veya istemci IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağda çalışır)", + "Duration": "Süre", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Torrentlerin başlangıç durumu qBittorrent'e eklendi. Zorunlu Torrentlerin seed kısıtlamalarına uymadığını unutmayın", + "DownloadClientRTorrentSettingsUrlPath": "URL Yolu", + "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.", + "DownloadClientSettingsInitialState": "Başlangıç Durumu", + "DownloadClientSettingsUrlBaseHelpText": "{clientName} URL'sine {url} gibi bir önek ekler", + "DownloadClientSettingsInitialStateHelpText": "{clientName} dosyasına eklenen torrentler için başlangıç durumu", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Etkinleştirme, durdurulmuş durumdaki rTorrent'e torrentler ve magnet ekleyecektir. Bu magnet dosyalarını bozabilir.", + "DownloadClientSettingsAddPaused": "Duraklatılana Ekle", + "DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın", + "DownloadClientSettingsUseSslHelpText": "{clientName} ile bağlantı kurulurken güvenli bağlantıyı kullan", + "DownloadClientTransmissionSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan İletim konumunu kullanmak için boş bırakın", + "DownloadClientPneumaticSettingsStrmFolder": "Strm Klasörü", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Bu klasördeki .strm dosyaları drone ile içe aktarılacak", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Önce ilk ve son parçaları indirin (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Sıralı Sıra", + "DownloadClientRTorrentSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan rTorrent konumunu kullanmak için boş bırakın", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "{clientName} rpc URL'sine bir önek ekler, örneğin {url}, varsayılan olarak '{defaultUrl}' olur", + "DownloadClientQbittorrentSettingsContentLayout": "İçerik Düzeni", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb Klasörü", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Bu seçenek en az NzbGet sürüm 16.0'ı gerektirir", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Bu klasöre XBMC'den erişilmesi gerekecek", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Sıralı olarak indirin (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Güvenli bir bağlantı kullanın. qBittorrent'te Seçenekler -> Web Kullanıcı Arayüzü -> 'HTTP yerine HTTPS kullan' bölümüne bakın.", + "DownloadClientRTorrentSettingsAddStopped": "Durdurulana Ekle", + "Label": "Etiket", + "EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle", + "EditSelectedIndexers": "Seçili İndeksleyicileri Düzenle", + "NoIndexersFound": "İndeksleyici bulunamadı", + "NoHistoryFound": "Geçmiş bulunamadı", + "ManageDownloadClients": "İndirme İstemcilerini Yönet", + "InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için", + "LabelIsRequired": "Etiket gerekli", + "ManageClients": "İstemcileri Yönet", + "Implementation": "Uygula", + "NotificationsEmailSettingsUseEncryptionHelpText": "Sunucuda yapılandırılmışsa şifrelemeyi kullanmayı mı tercih edeceğiniz, şifrelemeyi her zaman SSL (yalnızca Bağlantı Noktası 465) veya StartTLS (başka herhangi bir bağlantı noktası) aracılığıyla mı kullanacağınız veya hiçbir zaman şifrelemeyi kullanmama tercihlerini belirler", + "Menu": "Menü", + "NotificationsEmailSettingsUseEncryption": "Şifreleme Kullan", + "InvalidUILanguage": "Kullanıcı arayüzünüz geçersiz bir dile ayarlanmış, düzeltin ve ayarlarınızı kaydedin", + "InstanceName": "Örnek isim", + "NoDownloadClientsFound": "İndirme istemcisi bulunamadı", + "NotificationTriggersHelpText": "Bu bildirimi hangi olayların tetikleyeceğini seçin", + "Theme": "Tema", + "OnHealthRestored": "Sağlığın İyileştirilmesi Hakkında", + "NotificationsTelegramSettingsIncludeAppName": "{appName}'i Başlığa dahil et", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Farklı uygulamalardan gelen bildirimleri ayırt etmek için isteğe bağlı olarak mesaj başlığının önüne {appName} ekleyin", + "UseSsl": "SSL kullan", + "OnApplicationUpdate": "Uygulama Güncellemesinde", + "TorrentBlackholeSaveMagnetFiles": "Magnet Dosyalarını Kaydet", + "SecretToken": "Gizlilik Token'ı", + "TorrentBlackholeSaveMagnetFilesExtension": "Magnet Dosya Uzantısını Kaydet", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet bağlantıları için kullanılacak uzantı, varsayılan olarak '.magnet'tir", + "TorrentBlackholeSaveMagnetFilesHelpText": ".torrent dosyası yoksa magnet bağlantısını kaydedin (yalnızca indirme istemcisi bir dosyaya kaydedilen magnetleri destekliyorsa kullanışlıdır)", + "TorrentBlackholeTorrentFolder": "Torrent Klasörü", + "PasswordConfirmation": "Şifre Tekrarı", + "StopSelecting": "Düzenlemeden Çık", + "Started": "Başlatıldı", + "UsenetBlackholeNzbFolder": "Nzb Klasörü", + "XmlRpcPath": "XML RPC Yolu", + "IndexerSettingsSeedTime": "Seed Süresi", + "IndexerSettingsSeedRatio": "Seed Oranı", + "IndexerSettingsSeedTimeHelpText": "Bir torrentin durdurulmadan önce ulaşması gereken oran, boş bırakıldığında uygulamanın varsayılanı kullanılır", + "IndexerSettingsSeedRatioHelpText": "Bir torrentin durdurulmadan önce ulaşması gereken oran. Boş bırakılırsa indirme istemcisinin varsayılan değerini kullanır. Oran en az 1,0 olmalı ve indeksleyici kurallarına uygun olmalıdır", + "External": "Harici", + "Notifications": "Bildirimler", + "CountApplicationsSelected": "{count} uygulama seçildi", + "SeedRatio": "Seed Oranı", + "ThemeHelpText": "Uygulama UI Temasını Değiştirin, 'Otomatik' Tema, işletim sisteminizde kullandığınız Açık veya Koyu moda göre ayarlanır . {inspiredBy} tarafından esinlenilmiştir.", + "Notification": "Bildirimler", + "SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin", + "EditSyncProfile": "Senkronizasyon Profilini Düzenle", + "UserAgentProvidedByTheAppThatCalledTheAPI": "API'yi çağıran uygulama tarafından sağlanan Kullanıcı Aracısı", + "Author": "Yazar", + "IndexerHDBitsSettingsMediums": "Medyalar", + "OnHealthRestoredHelpText": "Sağlığın İyileştirilmesi Hakkında", + "SeedTime": "Seed Süresi", + "IndexerHDBitsSettingsCodecs": "Kodekler", + "Publisher": "Yayımcı", + "OnApplicationUpdateHelpText": "Uygulama Güncellemesinde", + "DeleteSelectedApplicationsMessageText": "Seçili {count} uygulamayı silmek istediğinizden emin misiniz?", + "ProxyValidationBadRequest": "Proxy ile test edilemedi. DurumKodu: {statusCode}", + "UpdateAvailableHealthCheckMessage": "Yeni güncelleme mevcut: {version}", + "days": "gün", + "Default": "Varsayılan", + "GrabRelease": "Sürümü Al", + "ManualGrab": "Manuel Alımlarda", + "OverrideAndAddToDownloadClient": "Geçersiz kıl ve indirme istemcisine ekle", + "OverrideGrabModalTitle": "Geçersiz Kıl ve Al - {title}", + "PrioritySettings": "Öncelik: {priority}", + "IndexerDownloadClientHealthCheckMessage": "Geçersiz indirme istemcilerine sahip indeksleyiciler: {indexerNames}.", + "BuiltIn": "Dahili", + "Script": "Komut Dosyası", + "InfoUrl": "Bilgi URL'si", + "PublishedDate": "Yayınlanma Tarihi", + "Any": "Herhangi", + "AllSearchResultsHiddenByFilter": "Uygulanan filtre nedeniyle tüm arama sonuçları gizlendi.", + "HealthMessagesInfoBox": "Bu sağlık kontrolü mesajlarının nedeni hakkında daha fazla bilgiyi, satırın sonundaki wiki bağlantısına (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bulabilirsiniz. Bu mesajları yorumlamada zorluk çekiyorsanız, aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.", + "PackageVersionInfo": "{packageAuthor} tarafından {packageVersion}", + "LogSizeLimit": "Log Boyutu Sınırı", + "LogSizeLimitHelpText": "Arşivlemeden önce MB cinsinden maksimum log dosya boyutu. Varsayılan 1 MB'tır.", + "AptUpdater": "Güncellemeyi yüklemek için apt'ı kullanın", + "Download": "İndir", + "ErrorRestoringBackup": "Yedekleme geri yüklenirken hata oluştu", + "ExternalUpdater": "{appName} harici bir güncelleme mekanizması kullanacak şekilde yapılandırılmıştır", + "LogFilesLocation": "Log kayıtlarının bulunduğu konum: {location}", + "NoEventsFound": "Etkinlik bulunamadı", + "RestartReloadNote": "Not: {appName} geri yükleme işlemi sırasında otomatik olarak yeniden başlatılacak ve kullanıcı arayüzünü yeniden yükleyecektir.", + "TheLogLevelDefault": "Log seviyesi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/ayarlar/genel) bölümünden değiştirilebilir", + "UpdateAppDirectlyLoadError": "{appName} doğrudan güncellenemiyor,", + "DockerUpdater": "Güncellemeyi almak için docker konteynerini güncelleyin", + "FailedToFetchUpdates": "Güncellemeler alınamadı", + "Logout": "Çıkış", + "UpdaterLogFiles": "Log Kayıt Güncelleyici", + "WouldYouLikeToRestoreBackup": "'{name}' yedeğini geri yüklemek ister misiniz?", + "InstallLatest": "En Son Sürümü Yükle", + "Install": "Kur", + "InstallMajorVersionUpdate": "Güncellemeyi Kur", + "InstallMajorVersionUpdateMessage": "Bu güncelleştirme yeni bir ana sürüm yükleyecek ve sisteminizle uyumlu olmayabilir. Bu güncelleştirmeyi yüklemek istediğinizden emin misiniz?", + "InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin.", + "Season": "Sezon", + "Artist": "Sanatçı", + "Mixed": "Karışık", + "Stats": "Durum", + "CurrentlyInstalled": "Şuan Kurulu", + "PreviouslyInstalled": "Daha Önce Kurulmuş", + "FailedToFetchSettings": "Ayarlar alınamadı", + "IndexerSettingsCookie": "Çerez", + "IndexerHDBitsSettingsMediumsHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "IndexerSettingsApiPathHelpText": "API'ye giden yol, genellikle {url}", + "IndexerSettingsAdditionalParameters": "Ek Parametreler", + "IndexerSettingsApiPath": "API Yolu", + "IndexerHDBitsSettingsCodecsHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "IndexerHDBitsSettingsOriginsHelpText": "Belirtilmezse tüm seçenekler kullanılır.", + "MinimumSeeders": "Minimum Seeder", + "AuthQueries": "Yetkilendirme Talepleri", + "AreYouSureYouWantToDeleteIndexer": "'{name}' uygulamasını {appName} uygulamasından silmek istediğinizden emin misiniz?", + "Auth": "Yetkilendirme", + "AverageQueries": "Talep Ortalaması", + "AverageResponseTimesMs": "Ortalama İndeksleyici Yanıt Süreleri (ms)", + "BookSearch": "Kitap Ara", + "DeleteIndexerProxy": "İndeksleyici Proxy'sini Sil", + "DownloadClientSettingsPriorityItemHelpText": "Öğeleri alırken kullanılacak öncelik", + "EditCategory": "Kategoriyi Düzenle", + "EnableIndexer": "İndeksleyiciyi Etkinleştir", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "SAHNE sürümlerini sonuçlardan hariç tut", + "IndexerBeyondHDSettingsRefundOnly": "Sadece İade", + "DefaultCategory": "Varsayılan Kategori", + "GrabTitle": "Başlığı Al", + "ApplicationsLoadError": "Uygulama listesi yüklenemiyor", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "Sadece freeleech'te ara (Sınırlı UL)", + "AreYouSureYouWantToDeleteCategory": "Haritalanan kategoriyi silmek istediğinizden emin misiniz?", + "AverageGrabs": "Ortalama Alım Sayısı", + "DevelopmentSettings": "Geliştirme Ayarları", + "IndexerBeyondHDSettingsRssKeyHelpText": "Siteden RSS Anahtarı (Güvenlik => RSS Anahtarı)", + "ApplicationTagsHelpTextWarning": "Etiketler istenmeyen etkilere neden olabileceğinden dikkatli kullanılmalıdır. Etiketi olan bir uygulama yalnızca aynı etikete sahip indeksleyicilerı eşitler.", + "DeleteApplication": "Uygulamayı Sil", + "IndexerDownloadClientHelpText": "Bu indeksleyiciden {appName} içinde yapılan alımlar için hangi indirme istemcisinin kullanılacağını belirtin", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "Bir sürüm için eşlenen kategori yoksa varsayılan olarak kullanılacak yedek kategori. {appName}'a özel bir kategori eklemek, {appName} ile ilgisi olmayan indirmelerle çakışmaları önlemeye yardımcı olur. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir. Çıkış dizininde bir alt dizin [kategori] oluşturur.", + "IncludeManualGrabsHelpText": "{appName} uygulamasında yapılan manuel alımları dahil et", + "IndexerAlphaRatioSettingsExcludeScene": "SAHNE'yi hariç tut", + "AudioSearch": "Müzik Ara", + "IndexerDisabled": "İndeksleyici Devre Dışı", + "ClearHistoryMessageText": "{appName} geçmişinin tamamını temizlemek istediğinizden emin misiniz?", + "ClearHistory": "Geçmişi Temizle", + "ElapsedTime": "Geçen Süre", + "EnabledRedirected": "Etkinleştirildi, Yönlendirildi", + "IndexerAuth": "İndeksleyici Kimlik Doğrulaması", + "EnableRssHelpText": "İndeksleyici için RSS beslemesini etkinleştirin", + "Description": "Tanım", + "AppsMinimumSeeders": "Uygulama İçin Azami Seeder", + "AppsMinimumSeedersHelpText": "Uygulamaların gerektirdiği veri kaynağından alınacak öğelerin minimum seeder sayısı. Boş bırakılırsa varsayılan senkronizasyon profili kullanılır", + "BasicSearch": "Temel Arama", + "DeleteSelectedApplications": "Seçili Uygulamaları Sil", + "DeleteSelectedIndexers": "Seçili İndeksleyicileri Sil", + "IndexerCategories": "İndeksleyici Kategorileri", + "Encoding": "Kodlama", + "FullSync": "Tam Senkronizasyon", + "GoToApplication": "Uygulamaya git", + "BookSearchTypes": "Kitap Arama Türleri", + "Id": "KİMLİK", + "DeleteSelectedIndexer": "Seçili İndeksleyiciyi Sil", + "DeleteAppProfile": "Uygulama Profilini Sil", + "DeleteClientCategory": "İndirme İstemcisi Kategorisini Sil", + "HistoryDetails": "Geçmiş Ayrıntıları", + "IndexerFailureRate": "İndeksleyici Arıza Oranı", + "IndexerSettingsCookieHelpText": "Site Çerezi", + "DisabledUntil": "Şu tarihe kadar devre dışı bırakıldı", + "DownloadClientsSettingsSummary": "{appName} kullanıcı arayüzü aramasına entegrasyon için indirme istemcisi yapılandırması", + "Apps": "Uygulamalar", + "ApplicationTagsHelpText": "Etiketi olmayan veya bir veya daha fazla eşleşen etiketi olan indeksleyicilerı bu uygulamayla senkronize edin. Burada hiçbir etiket listelenmezse, etiketleri nedeniyle hiçbir indeksleyicinun senkronizasyonu engellenmeyecektir.", + "Book": "Kitap", + "DownloadClientCategory": "İstemci Kategorisini İndirin", + "FilterPlaceHolder": "İndeksleyici Ara", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerAlreadySetup": "Dizin oluşturucunun en az bir örneği zaten yapılandırılmış", + "IndexerBeyondHDSettingsLimitedOnly": "Sadece Sınırlı", + "ClickToChangeQueryOptions": "Talep seçeneklerini değiştirmek için tıklayın", + "CountIndexersAvailable": "{count} indeksleyici mevcut", + "DownloadClientSettingsDefaultCategoryHelpText": "Bir sürüm için eşlenen kategori yoksa varsayılan olarak kullanılacak yedek kategori. {appName}'a özel bir kategori eklemek, {appName} ile ilgisi olmayan indirmelerle çakışmaları önlemeye yardımcı olur. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir.", + "IndexerBeyondHDSettingsRewindOnly": "Sadece Geri Sar", + "FoundCountReleases": "{itemCount} sürüm bulundu", + "HistoryCleanup": "Geçmiş Temizliği", + "IndexerSettingsPasskey": "Geçiş Anahtarı", + "IndexerAvistazSettingsPasswordHelpText": "Site Şifresi", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerAvistazSettingsUsernameHelpText": "Site Kullanıcı Adı", + "IndexerAvistazSettingsPidHelpText": "Hesabım veya Profilim sayfasının PID'si", + "IndexerAvistazSettingsUsernameHelpTextWarning": "Bu indeksleyicinin API'si yalnızca üyeler ve üzeri kullanıcılar tarafından kullanılabilir.", + "IndexerBeyondHDSettingsApiKeyHelpText": "Site API anahtarı (Güvenlik => API Anahtarı)", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Sadece iadeyi ara", + "IndexerBeyondHDSettingsRewindOnlyHelpText": "Sadece tekrarları ara", + "IndexerBeyondHDSettingsSearchTypes": "Arama Türleri", + "IndexerBeyondHDSettingsSearchTypesHelpText": "İlginizi çeken sürüm türlerini seçin. Hiçbiri seçilmezse, tüm seçenekler kullanılır.", + "IndexerDetails": "İndeksleyici Ayrıntıları", + "IndexerFileListSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerPassThePopcornSettingsApiUserHelpText": "Bu ayarlar PassThePopcorn güvenlik ayarlarınızda (Profil Düzenle > Güvenlik) bulunur.", + "IndexerProxies": "İndeksleyici Proxy'leri", + "NewznabUrl": "Newznab URL'si", + "Open": "Açık", + "SearchCapabilities": "Arama Yetenekleri", + "TestAllApps": "Tüm Uygulamaları Test Et", + "IndexerVipExpiringHealthCheckMessage": "İndeksleyici VIP avantajlarının süresi yakında doluyor: {indexerNames}", + "IndexerVipExpiredHealthCheckMessage": "İndeksleyici VIP avantajları sona erdi: {indexerNames}", + "IndexerGazelleGamesSettingsSearchGroupNames": "Grup Adlarını Ara", + "IndexerSettingsGrabLimit": "Alım Sınırı", + "PreferMagnetUrl": "Magnet URL'sini tercih edin", + "PreferMagnetUrlHelpText": "Etkinleştirildiğinde, bu indeksleyici torrent bağlantılarına geri dönüş için magnet URL'lerinin kullanımını tercih edecektir", + "Redirected": "Yönlendirildi", + "SearchCountIndexers": "{count} indeksleyiciyi ara", + "SearchType": "Arama Türü", + "SyncAppIndexers": "İndeksleyicileri Senkronize Et", + "TVSearchTypes": "TV Arama Türleri", + "TotalIndexerQueries": "Toplam İndeksleyici Sorguları", + "TotalUserAgentQueries": "Kullanıcı Aracısı Başına Toplam Sorgu Sayısı", + "LastFailure": "Son Hata", + "RssQueries": "RSS Sorguları", + "ProxyValidationUnableToConnect": "Proxy'ye bağlanılamıyor: {exceptionMessage}. Ayrıntılar için bu hatayla ilgili günlüğü kontrol edin", + "QueryResults": "Sorgu Sonuçları", + "IndexerFileListSettingsUsernameHelpText": "Site Kullanıcı Adı", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerHDBitsSettingsUseFilenamesHelpText": "Torrent dosya adlarını sürüm başlıkları olarak kullanmak istiyorsanız bu seçeneği işaretleyin", + "IndexerHDBitsSettingsUsernameHelpText": "Site Kullanıcı Adı", + "IndexerHistoryLoadError": "İndeksleyici geçmişi yüklenirken hata oluştu", + "IndexerIPTorrentsSettingsCookieUserAgent": "Çerezler için Kullanıcı Aracısı", + "IndexerId": "İndeksleyici Kimliği", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerNebulanceSettingsApiKeyHelpText": "API Anahtarı Kullanıcı Ayarları > Api Anahtarları'ndan. Anahtarın Listeleme ve İndirme izinlerine sahip olması gerekir", + "IndexerNewznabSettingsVipExpirationHelpText": "VIP Son Kullanma Tarihi için (yyyy-aa-gg) tarihini girin veya boş bırakın, {appName} VIP'nin sona ermesinden 1 hafta sonra bildirimde bulunacaktır", + "IndexerSettingsApiUser": "API Kullanıcısı", + "IndexerSettingsBaseUrl": "Temel URL", + "IndexerSettingsBaseUrlHelpText": "{appName}'ın siteye yönelik istekler için hangi temel URL'yi kullanacağını seçin", + "IndexerSettingsLimitsUnit": "Limit Birimi", + "IndexerSettingsLimitsUnitHelpText": "İndeksleyici başına limitleri saymak için zaman birimi", + "IndexerSettingsRssKey": "RSS Anahtarı", + "MinimumSeedersHelpText": "İndeksleyicinin alım yapması için uygulamanın gerektirdiği minimum seeder sayısı", + "MusicSearchTypes": "Müzik Arama Türleri", + "NoApplicationsFound": "Hiçbir uygulama bulunamadı", + "OnGrabHelpText": "Yayın Alındığında", + "PackSeedTime": "Paket Seed Süresi", + "ProwlarrDownloadClientsAlert": "{appName} içinde doğrudan arama yapmayı düşünüyorsanız, İndirme İstemcileri eklemeniz gerekir. Aksi takdirde, bunları buraya eklemeniz gerekmez. Uygulamalarınızdan yapılan aramalar için, bunun yerine orada yapılandırılan indirme istemcileri kullanılır.", + "UnableToLoadAppProfiles": "Uygulama profilleri yüklenemiyor", + "UnableToLoadDevelopmentSettings": "Geliştirme ayarları yüklenemiyor", + "IndexerHDBitsSettingsOrigins": "Kaynaklar", + "IndexerNoDefinitionCheckHealthCheckMessage": "{indexerNames} indeksleyicileri tanımsız olduğundan çalışmayacaktır. Lütfen bunları kaldırın ve/veya {appName} uygulamasına yeniden ekleyin.", + "MovieSearchTypes": "Film Arama Türleri", + "NoIndexerCategories": "Bu indeksleyici için hiçbir kategori bulunamadı", + "PackSeedTimeHelpText": "Bir paketin (sezon veya diskografi) torrentinin durdurulmadan önce aktif olması gereken süre, boş bırakılırsa uygulamanın varsayılan değeri kullanılır", + "IndexerSettingsGrabLimitHelpText": "{appName}'ın siteye izin vereceği ilgili birim tarafından belirtilen maksimum kapma sayısı", + "IndexerSettingsPreferMagnetUrlHelpText": "Etkinleştirildiğinde, bu indeksleyici torrent bağlantılarına geri dönüş için magnet URL'lerinin kullanımını tercih edecektir", + "Private": "Özel", + "ProwlarrSupportsAnyIndexer": "{appName}, 'Generic Newznab' (usenet için) veya 'Generic Torznab' (torrentler için) kullanan Newznab/Torznab standardını kullanan herhangi bir indeksleyiciye ek olarak birçok indeksleyiciyi destekler. Aşağıdan indeksleyicinizi arayın ve seçin.", + "IndexerInfo": "İndeksleyici Bilgileri", + "IndexerMTeamTpSettingsApiKeyHelpText": "Siteden API Anahtarı (Kullanıcı Kontrol Paneli => Güvenlik => Laboratuvar'da Bulunur)", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerRedactedSettingsApiKeyHelpText": "Siteden API Anahtarı (Ayarlar => Erişim Ayarları'nda Bulunur)", + "IndexerSettingsQueryLimitHelpText": "{appName}'in siteye izin vereceği ilgili birim tarafından belirtilen maksimum sorgu sayısı", + "IndexerSettingsVipExpiration": "VIP Son Kullanma Tarihi", + "Parameters": "Parametreler", + "TotalQueries": "Toplam Sorgular", + "SettingsIndexerLogging": "Gelişmiş İndeksleyici Günlüğü", + "SettingsLogRotate": "Günlük Döndürme", + "TotalIndexerSuccessfulGrabs": "İndeksleyicinin Toplam Başarılı Alma Sayısı", + "SyncProfiles": "Profilleri Senkronize Et", + "IndexerProxy": "İndeksleyici Proxy", + "Redirect": "Yönlendir", + "SettingsSqlLoggingHelpText": "{appName} uygulamasından gelen tüm SQL sorgularını günlüğe kaydet", + "Proxies": "Proxy'ler", + "Public": "Herkese Açık", + "RepeatSearch": "Tekrar Ara", + "ManageApplications": "Uygulamaları Yönet", + "SearchAllIndexers": "Tüm indeksleyicilerde ara", + "SeedRatioHelpText": "Bir torrentin durdurulmadan önce ulaşması gereken oran, boş bırakıldığında uygulamanın varsayılanı kullanılır", + "SeedTimeHelpText": "Bir torrentin durdurulmadan önce aktif olması gereken süre, boş bırakıldığında uygulamanın varsayılanı kullanılır", + "IndexerRss": "İndeksleyici RSS", + "SearchTypes": "Arama Türleri", + "SemiPrivate": "Yarı Özel", + "SettingsLogSql": "Sql Günlüğü", + "SelectedCountOfCountReleases": "{itemCount} sürümden {selectedCount} tanesi seçildi", + "IndexerSettingsSummary": "Proxy'ler de dahil olmak üzere çeşitli genel indeksleyici ayarlarını yapılandırın.", + "SettingsLogRotateHelpText": "Günlük klasöründe saklanacak maksimum günlük dosyası sayısı", + "TvSearch": "Dizi Ara", + "SelectIndexers": "İndeksleyici Seç", + "Privacy": "Gizlilik", + "SettingsConsoleLogLevel": "Konsol Günlük Düzeyi", + "SettingsFilterSentryEventsHelpText": "Bilinen kullanıcı hatası olaylarının Analitik olarak gönderilmesini filtreleyin", + "SyncLevel": "Senkronizasyon Seviyesi", + "IndexerName": "İndeksleyici Adı", + "IndexerSite": "İndeksleyici Sitesi", + "IndexerTagsHelpTextWarning": "Etiketler dikkatli kullanılmalıdır, istenmeyen etkilere neden olabilirler. Etiketli bir indeksleyici yalnızca aynı etikete sahip uygulamalarla senkronize olur.", + "InitialFailure": "İlk Hata", + "MappedCategories": "Haritalanmış Kategoriler", + "NotSupported": "Desteklenmiyor", + "SettingsIndexerLoggingHelpText": "Yanıt dahil olmak üzere ek İndeksleyici verilerini günlüğe kaydet", + "SearchQueries": "Arama Sorguları", + "SyncProfile": "Profil Senkronizasyonu", + "NoSearchResultsFound": "Hiçbir arama sonucu bulunamadı, aşağıdan yeni bir arama yapmayı deneyin.", + "RssFeed": "RSS Beslemesi", + "VipExpiration": "VIP Son Kullanma Tarihi", + "IndexerPassThePopcornSettingsApiKeyHelpText": "Site API Anahtarı", + "IndexerSettingsFreeleechOnly": "Sadece Freeleech", + "NoIndexerHistory": "Bu indeksleyici için geçmiş bulunamadı", + "SearchIndexers": "İndeksleyicilerde Ara", + "Url": "Url", + "Website": "Web site", + "IndexerTagsHelpText": "Etiketleri kullanarak İndeksleyici Proxy'lerini veya İndeksleyici'nin hangi uygulamalarla senkronize edileceğini belirtin.", + "MassEditor": "Kitle Editörü", + "RawSearchSupported": "Ham Arama Destekleniyor", + "SettingsFilterSentryEvents": "Analitik Olayları Filtrele", + "TorznabUrl": "Torznab Url", + "TotalHostGrabs": "İstemci Başına Toplam Alım", + "TotalUserAgentGrabs": "Kullanıcı Aracısı Başına Toplam Alım Sayısı", + "IndexerGazelleGamesSettingsApiKeyHelpText": "Siteden API Anahtarı (Ayarlar => Erişim Ayarları'nda Bulunur)", + "IndexerObsoleteCheckMessage": "İndeksleyiciler eski veya güncellendi: {0}. Lütfen {appName}'i kaldırın ve (veya) yeniden ekleyin", + "IndexerNzbIndexSettingsApiKeyHelpText": "Site API Anahtarı", + "IndexerOrpheusSettingsApiKeyHelpText": "Siteden API Anahtarı (Ayarlar => Erişim Ayarları'nda Bulunur)", + "IndexerHDBitsSettingsPasskeyHelpText": "Kullanıcı Detaylarından Geçiş Anahtarı", + "IndexerQuery": "İndeksleyici Sorgusu", + "MovieSearch": "Film Arama", + "TotalGrabs": "Toplam Alınan", + "TotalHostQueries": "İstemci Başına Toplam Sorgu", + "Query": "Sorgu", + "QueryOptions": "Sorgu Seçenekleri", + "SyncLevelAddRemove": "Yalnızca Ekle ve Kaldır: {appName} uygulamasından indeksleyiciler eklendiğinde veya kaldırıldığında, bu uzak uygulama güncellenecektir.", + "IndexerIPTorrentsSettingsCookieUserAgentHelpText": "Tarayıcıdan kullanılan çerezle ilişkili Kullanıcı Aracısı", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini arayın", + "IndexerSettingsAppsMinimumSeedersHelpText": "Uygulamalar tarafından indeksleyicinin alım yapması için gereken minimum sedeerlar, boş bırakılırsa Eşitleme profilinin varsayılanı kullanılacaktır", + "IndexerSettingsQueryLimit": "Sorgu Limiti", + "IndexerHDBitsSettingsFreeleechOnlyHelpText": "Yalnızca freeleech sürümlerini göster", + "IndexerHDBitsSettingsUseFilenames": "Dosya Adlarını Kullan", + "IndexerNewznabSettingsAdditionalParametersHelpText": "Ek Newznab parametreleri", + "IndexerNewznabSettingsApiKeyHelpText": "Site API Anahtarı", + "IndexerSettingsAppsMinimumSeeders": "Uygulamalar Minimum Seeders", + "RedirectHelpText": "İndeksleyici için gelen indirme isteğini yeniden yönlendirin ve isteği {appName} aracılığıyla proxy olarak göndermek yerine doğrudan alım yapmayı tercih edin", + "SyncLevelFull": "Tam Senkronizasyon: Bu uygulamanın indeksleyicilerini tamamen senkronize halde tutar. {appName} içindeki indeksleyicilerde yapılan değişiklikler daha sonra bu uygulamayla senkronize edilir. Bu uygulama içinde indeksleyicilerde uzaktan yapılan herhangi bir değişiklik, bir sonraki senkronizasyonda {appName} tarafından geçersiz kılınır.", + "IndexerSettingsPreferMagnetUrl": "Magnet URL'sini Tercih Et", + "IndexerTorrentSyndikatSettingsApiKeyHelpText": "Site API Anahtarı", + "ProwlarrDownloadClientsInAppOnlyAlert": "İndirme istemcileri yalnızca {appName} uygulama içi aramalar içindir ve uygulamalarla senkronize edilmez. Geliştiricilerin bu tür bir işlevsellik ekleme planı yoktur.", + "ProwlarrSupportsAnyDownloadClient": "{appName} aşağıda listelenen indirme istemcilerini destekler.", + "UnableToLoadIndexerProxies": "İndeksleyici Proxy'leri yüklenemiyor", + "IndexerStatus": "İndeksleyici Durumu", + "IndexerPassThePopcornSettingsGoldenPopcornOnly": "Sadece Golden Popcorn", + "IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Yalnızca Golden Popcorn sürümlerini arayın", + "IndexerSettingsPackSeedTime": "Paket Seed Süresi", + "IndexerSettingsPackSeedTimeIndexerHelpText": "Bir paketin (sezon veya diskografi) torrentinin durdurulmadan önce edilmesi gereken süre, boş bırakılırsa varsayılan değeri kullanılır", + "IndexerFileListSettingsPasskeyHelpText": "Site Parolası (Bu, indirme istemcinizde gösterilen izleyici URL'sindeki alfanümerik dizedir)", + "IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "Kullanıcı ve Torrent izinlerine sahip olmalısınız", + "IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Grup adlarına göre sürüm ara", + "IndexerHealthCheckNoIndexers": "Hiçbir indeksleyici etkinleştirilmedi, {appName} arama sonuçlarını döndürmeyecek", + "QueryType": "Sorgu Türü", + "DownloadClientUTorrentProviderMessage": "uTorrent'in kripto para madenciliği, kötü amaçlı yazılım ve reklam içerme geçmişi vardır, bu nedenle farklı bir istemci seçmenizi önemle tavsiye ederiz." } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 936fa31db..f42bd890b 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -1,7 +1,7 @@ { "BranchUpdateMechanism": "Гілка, що використовується зовнішнім механізмом оновлення", "CancelPendingTask": "Ви впевнені, що хочете скасувати це незавершене завдання?", - "About": "Деталі", + "About": "Про нас", "AcceptConfirmationModal": "Вікно підтвердження", "Actions": "Дії", "Add": "Додати", @@ -11,8 +11,8 @@ "Analytics": "Аналітика", "ApiKey": "API Ключ", "Added": "Додано", - "AddIndexer": "Додати індексатор", - "AddingTag": "Додавання тегу", + "AddIndexer": "Додати Індексер", + "AddingTag": "Додавання тега", "AppDataDirectory": "Каталог AppData", "AppDataLocationHealthCheckMessage": "Оновлення буде неможливим, щоб запобігти видаленню AppData під час оновлення", "Apply": "Застосувати", @@ -29,19 +29,19 @@ "Database": "База даних", "Date": "Дата", "Dates": "Дати", - "DBMigration": "Міграція БД", + "DatabaseMigration": "Міграція бази даних", "DeleteBackup": "Видалити резервну копію", - "DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію '{0}'?", + "DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію \"{name}\"?", "DeleteDownloadClient": "Видалити клієнт завантаження", - "DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?", - "Authentication": "Аутентифікація", - "Automatic": "Автоматичний", + "DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{name}'?", + "Authentication": "Автентифікація", + "Automatic": "Автоматично", "AutomaticSearch": "Автоматичний пошук", "Backup": "Резервне копіювання", "BackupIntervalHelpText": "Інтервал між автоматичним резервним копіюванням", "Backups": "Резервні копії", "BeforeUpdate": "Перед оновленням", - "BindAddress": "Прив'язувати адресу", + "BindAddress": "Прив'язати адресу", "Branch": "Гілка", "BypassProxyForLocalAddresses": "Обійти проксі для локальних адрес", "Cancel": "Скасувати", @@ -53,7 +53,7 @@ "Close": "Закрити", "CloseCurrentModal": "Закрити поточне вікно", "Columns": "Колонки", - "AuthenticationMethodHelpText": "Для доступу до {appName} потрібні ім’я користувача та пароль", + "AuthenticationMethodHelpText": "Вимагати ім’я користувача та пароль для доступу до {appName}", "BackupFolderHelpText": "Відносні шляхи будуть у каталозі AppData {appName}", "BindAddressHelpText": "Дійсна адреса IP або '*' для всіх інтерфейсів", "BranchUpdate": "Гілка для оновлення {appName}", @@ -61,9 +61,9 @@ "AnalyticsEnabledHelpText": "Надсилайте анонімну інформацію про використання та помилки на сервери {appName}. Це включає інформацію про ваш веб-переглядач, які сторінки {appName} WebUI ви використовуєте, звіти про помилки, а також версію ОС і часу виконання. Ми будемо використовувати цю інформацію, щоб визначити пріоритети функцій і виправлення помилок.", "Delete": "Видалити", "DeleteApplicationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?", - "DeleteTagMessageText": "Ви впевнені, що хочете видалити тег {0} ?", + "DeleteTagMessageText": "Ви впевнені, що хочете видалити тег '{label}'?", "DeleteIndexerProxyMessageText": "Ви впевнені, що хочете видалити тег {0} ?", - "DeleteNotificationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?", + "DeleteNotificationMessageText": "Ви впевнені, що хочете видалити сповіщення '{name}'?", "YesCancel": "Так, скасувати", "InstanceName": "Ім'я екземпляра", "Interval": "Інтервал", @@ -72,7 +72,7 @@ "SSLCertPasswordHelpText": "Пароль для файлу pfx", "TestAll": "Перевірити все", "Type": "Тип", - "UnableToLoadDownloadClients": "Не вдалося завантажити клієнти для завантаження", + "DownloadClientsLoadError": "Не вдалося завантажити клієнти для завантаження", "UnableToLoadGeneralSettings": "Не вдалося завантажити загальні налаштування", "UnableToLoadHistory": "Не вдалося завантажити історію", "UnableToLoadIndexers": "Не вдалося завантажити індексатори", @@ -82,8 +82,8 @@ "Uptime": "Час роботи", "URLBase": "URL-адреса", "DownloadClients": "Клієнти завантажувачів", - "DownloadClientSettings": "Налаштування клієнта завантажувача", - "DownloadClientStatusCheckSingleClientMessage": "Завантаження клієнтів недоступне через помилки: {0}", + "DownloadClientSettings": "Налаштування клієнта завантаження", + "DownloadClientStatusSingleClientHealthCheckMessage": "Завантаження клієнтів недоступне через помилки: {downloadClientNames}", "Edit": "Редагувати", "EditIndexer": "Редагувати індексатор", "Docker": "Docker", @@ -108,8 +108,8 @@ "PackageVersion": "Версія пакета", "Protocol": "Протокол", "Proxy": "Проксі", - "ProxyCheckFailedToTestMessage": "Не вдалося перевірити проксі: {0}", - "ProxyCheckResolveIpMessage": "Не вдалося визначити IP-адресу для налаштованого проксі-сервера {0}", + "ProxyFailedToTestHealthCheckMessage": "Не вдалося перевірити проксі: {url}", + "ProxyResolveIpHealthCheckMessage": "Не вдалося визначити IP-адресу для налаштованого проксі-сервера {proxyHostName}", "ProxyType": "Тип проксі", "ProxyUsernameHelpText": "Вам потрібно лише ввести ім’я користувача та пароль, якщо вони потрібні. В іншому випадку залиште їх порожніми.", "Reset": "Скинути", @@ -134,7 +134,7 @@ "HomePage": "Домашня сторінка", "Hostname": "Ім'я хоста", "DeleteNotification": "Видалити сповіщення", - "IndexerStatusCheckSingleClientMessage": "Індексатори недоступні через помилки: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Індексатори недоступні через помилки: {indexerNames}", "Info": "Інформація", "InstanceNameHelpText": "Ім’я екземпляра на вкладці та ім’я програми Syslog", "InteractiveSearch": "Інтерактивний пошук", @@ -161,7 +161,7 @@ "Reload": "Перезавантажити", "RemovedFromTaskQueue": "Видалено з черги завдань", "Restore": "Відновлення", - "RSSIsNotSupportedWithThisIndexer": "Цей індексатор не підтримує RSS", + "RssIsNotSupportedWithThisIndexer": "Цей індексатор не підтримує RSS", "Save": "Зберегти", "SaveChanges": "Зберегти зміни", "SaveSettings": "Зберегти зміни", @@ -181,13 +181,13 @@ "UILanguageHelpTextWarning": "Потрібно перезавантажити браузер", "UnableToAddANewIndexerPleaseTryAgain": "Не вдалося додати новий індексатор, спробуйте ще раз.", "UnableToAddANewNotificationPleaseTryAgain": "Не вдалося додати нове сповіщення, спробуйте ще раз.", - "UnableToLoadBackups": "Не вдалося завантажити резервні копії", + "BackupsLoadError": "Не вдалося завантажити резервні копії", "UnableToLoadUISettings": "Не вдалося завантажити налаштування інтерфейсу користувача", "UnsavedChanges": "Незбережені зміни", "UnselectAll": "Скасувати вибір усіх", - "UpdateCheckStartupNotWritableMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{0}\" не може бути записана користувачем \"{1}\".", - "UpdateCheckStartupTranslocationMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{0}\" знаходиться в папці переміщення програми.", - "UpdateCheckUINotWritableMessage": "Неможливо встановити оновлення, оскільки папка інтерфейсу користувача \"{0}\" не може бути записана користувачем \"{1}\".", + "UpdateStartupNotWritableHealthCheckMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{startupFolder}\" не може бути записана користувачем \"{userName}\".", + "UpdateStartupTranslocationHealthCheckMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{startupFolder}\" знаходиться в папці переміщення програми.", + "UpdateUiNotWritableHealthCheckMessage": "Неможливо встановити оновлення, оскільки папка інтерфейсу користувача \"{uiFolder}\" не може бути записана користувачем \"{userName}\".", "Updates": "Оновлення", "UrlBaseHelpText": "Для підтримки зворотного проксі-сервера значення за умовчанням порожнє", "UseProxy": "Використовуйте проксі", @@ -230,7 +230,7 @@ "NoBackupsAreAvailable": "Немає резервних копій", "NoLinks": "Немає посилань", "OpenThisModal": "Відкрийте цей модальний вікно", - "ProxyCheckBadRequestMessage": "Не вдалося перевірити проксі. Код стану: {0}", + "ProxyBadRequestHealthCheckMessage": "Не вдалося перевірити проксі. Код стану: {statusCode}", "ReleaseStatus": "Статус випуску", "Refresh": "Оновити", "RefreshMovie": "Оновити фільм", @@ -250,11 +250,11 @@ "Enabled": "Увімкнено", "EnableInteractiveSearchHelpText": "Буде використано, коли використовується інтерактивний пошук", "Indexers": "Індексатори", - "HealthNoIssues": "Немає проблем із вашою конфігурацією", + "NoIssuesWithYourConfiguration": "Немає проблем із вашою конфігурацією", "IndexerFlags": "Прапори індексатора", - "IndexerLongTermStatusCheckAllClientMessage": "Усі індексатори недоступні через збої більше 6 годин", - "IndexerLongTermStatusCheckSingleClientMessage": "Індексатори недоступні через збої більше 6 годин: {0}", - "IndexerStatusCheckAllClientMessage": "Усі індексатори недоступні через збої", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Усі індексатори недоступні через збої більше 6 годин", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Індексатори недоступні через збої більше 6 годин: {indexerNames}", + "IndexerStatusAllUnavailableHealthCheckMessage": "Усі індексатори недоступні через збої", "LastExecution": "Останнє виконання", "LogFiles": "Файли журналів", "LogLevelTraceHelpTextWarning": "Журнал трасування слід увімкнути лише тимчасово", @@ -279,7 +279,7 @@ "Discord": "Discord", "DownloadClient": "Клієнт завантажувача", "Donations": "Пожертви", - "DownloadClientStatusCheckAllClientMessage": "Усі клієнти завантаження недоступні через збої", + "DownloadClientStatusAllClientHealthCheckMessage": "Усі клієнти завантаження недоступні через збої", "Enable": "Увімкнути", "Filename": "Ім'я файлу", "Host": "Хост", @@ -292,12 +292,12 @@ "Style": "Стиль", "SuggestTranslationChange": "Запропонуйте зміну перекладу", "TableOptionsColumnsMessage": "Виберіть, які стовпці відображаються та в якому порядку вони відображаються", - "SystemTimeCheckMessage": "Системний час вимкнено більш ніж на 1 день. Заплановані завдання можуть не працювати належним чином, доки час не буде виправлено", + "SystemTimeHealthCheckMessage": "Системний час вимкнено більш ніж на 1 день. Заплановані завдання можуть не працювати належним чином, доки час не буде виправлено", "OnGrab": "При захопленні", "SSLCertPath": "Шлях сертифіката SSL", "UI": "Інтерфейс користувача", "Reddit": "Reddit", - "RSS": "RSS", + "Rss": "RSS", "Seeders": "Сиди", "Wiki": "Wiki", "Grabbed": "Захоплений", @@ -309,7 +309,7 @@ "MaintenanceRelease": "Випуск для обслуговування: виправлення помилок та інші покращення. Щоб отримати докладнішу інформацію, перегляньте історію фіксації Github", "ThemeHelpText": "Змініть тему інтерфейсу додатка, тема «Авто» використовуватиме вашу тему ОС, щоб установити світлий або темний режим. Натхненний Theme.Park", "UILanguageHelpText": "Мова, яку {appName} використовуватиме для інтерфейсу користувача", - "UpdateMechanismHelpText": "Використовуйте вбудований засіб оновлення {appName} або скрипт", + "UpdateMechanismHelpText": "Використайте вбудоване оновлення {appName}'у або скрипт", "ApplicationStatusCheckSingleClientMessage": "Списки недоступні через помилки: {0}", "ApplicationStatusCheckAllClientMessage": "Усі списки недоступні через помилки", "LaunchBrowserHelpText": " Відкрийте веб-браузер і перейдіть на домашню сторінку {appName} під час запуску програми.", @@ -318,8 +318,8 @@ "EnableRss": "Увімкнути RSS", "HistoryCleanupDaysHelpText": "Встановіть значення 0, щоб вимкнути автоматичне очищення", "HistoryCleanupDaysHelpTextWarning": "Файли в кошику, старші за вибрану кількість днів, будуть очищені автоматично", - "IndexerProxyStatusCheckAllClientMessage": "Усі індексатори недоступні через збої", - "IndexerProxyStatusCheckSingleClientMessage": "Індексатори недоступні через помилки: {0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Усі індексатори недоступні через збої", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Індексатори недоступні через помилки: {indexerProxyNames}", "ReleaseBranchCheckOfficialBranchMessage": "Гілка {0} не є дійсною гілкою випуску {appName}, ви не отримуватимете оновлення", "UnableToAddANewApplicationPleaseTryAgain": "Не вдалося додати нове сповіщення, спробуйте ще раз.", "UnableToAddANewAppProfilePleaseTryAgain": "Не вдалося додати новий профіль якості, спробуйте ще раз.", @@ -336,15 +336,15 @@ "ApplicationLongTermStatusCheckSingleClientMessage": "Індексатори недоступні через збої більше 6 годин: {0}", "Remove": "Видалити", "Replace": "Замінити", - "TheLatestVersionIsAlreadyInstalled": "Остання версія {appName} вже встановлена", + "OnLatestVersion": "Остання версія {appName} вже встановлена", "ApplicationURL": "URL програми", "Theme": "Тема", "ApplyTagsHelpTextAdd": "Додати: додати теги до наявного списку тегів", "ApplyTagsHelpTextHowToApplyApplications": "Як застосувати теги до вибраних фільмів", "DeleteSelectedApplicationsMessageText": "Ви впевнені, що хочете видалити тег {0} ?", - "DeleteSelectedDownloadClients": "Видалити клієнт завантаження", + "DeleteSelectedDownloadClients": "Видалити клієнт(и) завантаження", "DeleteSelectedDownloadClientsMessageText": "Ви впевнені, що хочете видалити тег {0} ?", - "ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних фільмів", + "ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних індексаторів", "ApplyTagsHelpTextRemove": "Видалити: видалити введені теги", "DeleteSelectedIndexersMessageText": "Ви впевнені, що хочете видалити тег {0} ?", "DownloadClientPriorityHelpText": "Надайте пріоритет кільком клієнтам завантаження. Круговий алгоритм використовується для клієнтів з таким же пріоритетом.", @@ -353,20 +353,101 @@ "More": "Більше", "Track": "Трасувати", "Year": "Рік", - "UpdateAvailable": "Доступне нове оновлення", + "UpdateAvailableHealthCheckMessage": "Доступне нове оновлення", "Genre": "Жанри", - "ConnectionLostReconnect": "Radarr спробує підключитися автоматично, або ви можете натиснути перезавантажити нижче.", - "ConnectionLostToBackend": "Radarr втратив зв’язок із бекендом, і його потрібно перезавантажити, щоб відновити функціональність.", + "ConnectionLostReconnect": "{appName} спробує підключитися автоматично, або ви можете натиснути «Перезавантажити» нижче.", + "ConnectionLostToBackend": "{appName} втратив з’єднання з серверною частиною, і його потрібно перезавантажити, щоб відновити роботу.", "DeleteAppProfileMessageText": "Ви впевнені, що хочете видалити цей профіль затримки?", "RecentChanges": "Останні зміни", "minutes": "Хвилин", - "WhatsNew": "Що нового?", + "WhatsNew": "Що нового ?", "NotificationStatusAllClientHealthCheckMessage": "Усі списки недоступні через помилки", - "NotificationStatusSingleClientHealthCheckMessage": "Списки недоступні через помилки: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Списки недоступні через помилки: {notificationNames}", "AuthBasic": "Основний (спливаюче вікно браузера)", "AuthForm": "Форми (сторінка входу)", - "DisabledForLocalAddresses": "Відключено для локальних адрес", + "DisabledForLocalAddresses": "Вимкнено для локальних адрес", "None": "Жодного", "ResetAPIKeyMessageText": "Ви впевнені, що хочете скинути свій ключ API?", - "AddConnection": "Редагувати колекцію" + "AddConnection": "Додати Підключення", + "RestartProwlarr": "Перезавантажити {appName}", + "EditApplicationImplementation": "Додати умому", + "EditConnectionImplementation": "Додати умому", + "EditIndexerImplementation": "Додати умому", + "AddConnectionImplementation": "Додати Підключення - {implementationName}", + "AddApplicationImplementation": "Додати умому", + "AddIndexerImplementation": "Додати індексер - {implementationName}", + "AddIndexerProxyImplementation": "Додати умому", + "EditIndexerProxyImplementation": "Додати умому", + "AddCustomFilter": "Додати власний фільтр", + "AddDownloadClientImplementation": "Додати клієнт завантаження - {implementationName}", + "Application": "Додатки", + "CountApplicationsSelected": "Вибрано колекцій: {0}", + "Applications": "Додатки", + "Categories": "Категорії", + "EditDownloadClientImplementation": "Додати клієнт завантаження - {implementationName}", + "ApplyChanges": "Застосувати зміни", + "AuthenticationMethod": "Метод автентифікації", + "AuthenticationMethodHelpTextWarning": "Виберіть дійсний метод автентифікації", + "AppUpdated": "{appName} Оновлено", + "Album": "Альбом", + "AuthenticationRequired": "Потрібна Автентифікація", + "AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль", + "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача", + "Artist": "Виконавець", + "Yes": "Так", + "AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.", + "AppUpdatedVersion": "{appName} оновлено до версії `{version}`. Щоб отримати останні зміни, потрібно перезавантажити {appName}", + "AuthenticationRequiredHelpText": "Змінити запити, для яких потрібна автентифікація. Не змінюйте, якщо не розумієте ризики.", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Чи використовувати налаштований макет вмісту qBittorrent, оригінальний макет із торрента чи завжди створювати вкладену папку (qBittorrent 4.3.2+)", + "Category": "Категорія", + "BlackholeFolderHelpText": "Папка, у якій {appName} зберігатиме файл {extension}", + "DownloadClientFloodSettingsAdditionalTags": "Додаткові теги", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station", + "DownloadClientFloodSettingsUrlBaseHelpText": "Додає префікс до API Flood, наприклад {url}", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Спочатку завантажте першу та останню частини (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Завантаження в послідовному порядку (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Використовувати безпечне з'єднання. Дивіться параметри -> Web UI -> 'Use HTTPS instead of HTTP' в qBittorrent.", + "DownloadClientSettingsInitialStateHelpText": "Початковий стан для торрентів, доданих до {clientName}", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Підтвердити новий пароль", + "DownloadClientAriaSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2", + "ApiKeyValidationHealthCheckMessage": "Будь ласка оновіть ключ API, щоб він містив принаймні {length} символів. Ви можете зробити це в налаштуваннях або в файлі конфігурації", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2", + "IndexerHDBitsSettingsCodecs": "Кодек", + "DownloadClientSettingsUrlBaseHelpText": "Додає префікс до URL-адреси {connectionName}, наприклад {url}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2", + "ProxyValidationBadRequest": "Не вдалося перевірити проксі. Код стану: {statusCode}", + "CustomFilter": "Користувацькі фільтри", + "IndexerHDBitsSettingsMediums": "Середній", + "Default": "За замовчуванням", + "GrabRelease": "Захопити реліз", + "Clone": "Клонування", + "CountDownloadClientsSelected": "Вибрано {count} клієнтів завантажувача", + "Script": "Сценарій", + "Any": "Будь-який", + "BuiltIn": "Вбудований", + "PublishedDate": "Дата публікації", + "AllSearchResultsHiddenByFilter": "Всі результати приховані фільтром", + "AptUpdater": "Використовуйте apt для інсталяції оновлення", + "DockerUpdater": "Оновіть контейнер docker, щоб отримати оновлення", + "UpdateAppDirectlyLoadError": "Неможливо оновити {appName} безпосередньо,", + "Download": "Завантажити", + "ErrorRestoringBackup": "Помилка відновлення резервної копії", + "ExternalUpdater": "{appName} налаштовано на використання зовнішнього механізму оновлення", + "NoEventsFound": "Подій не знайдено", + "RestartReloadNote": "Примітка: {appName} автоматично перезапуститься та перезавантажить інтерфейс під час процесу відновлення.", + "InstallLatest": "Встановити останній", + "Mixed": "Виправлено", + "CurrentlyInstalled": "В даний час встановлено", + "Season": "Причина", + "Stats": "Статус", + "CountIndexersSelected": "{count} індексер(-и) обрано", + "SeedRatio": "Коефіцієнт роздачі", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "Якщо торрент заблоковано хешем, він може не бути належним чином відхилений під час RSS/пошуку для деяких індексаторів. Увімкнення цього параметра дозволить відхилити його після захоплення торента, але до його відправки клієнту.", + "MinimumSeeders": "Мінімум сидерів (роздаючих)", + "SeedTime": "Час сидіння", + "Author": "Автор", + "OnHealthRestoredHelpText": "При відновленні стану", + "IndexerHDBitsSettingsOriginsHelpText": "Якщо не вказано, використовуються всі параметри.", + "days": "дні(в)" } diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 3df1ac935..704eb38df 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -6,12 +6,12 @@ "CustomFilters": "Bộ lọc tùy chỉnh", "Date": "Ngày", "Dates": "ngày", - "DBMigration": "Di chuyển DB", + "DatabaseMigration": "Di chuyển DB", "NoBackupsAreAvailable": "Không có bản sao lưu nào", "NoChanges": "Không thay đổi", "NoLeaveIt": "Không để nó", "NoLogFiles": "Không có tệp nhật ký", - "IndexerProxyStatusCheckSingleClientMessage": "Danh sách không có sẵn do lỗi: {0}", + "IndexerProxyStatusUnavailableHealthCheckMessage": "Danh sách không có sẵn do lỗi: {indexerProxyNames}", "Indexers": "Người lập chỉ mục", "InteractiveSearch": "Tìm kiếm tương tác", "Message": "Thông điệp", @@ -35,8 +35,8 @@ "Priority": "Sự ưu tiên", "Protocol": "Giao thức", "ProxyBypassFilterHelpText": "Sử dụng ',' làm dấu phân tách và '*.' làm ký tự đại diện cho các miền phụ", - "ProxyCheckFailedToTestMessage": "Không thể kiểm tra proxy: {0}", - "ProxyCheckResolveIpMessage": "Không thể phân giải Địa chỉ IP cho Máy chủ Proxy đã Định cấu hình {0}", + "ProxyFailedToTestHealthCheckMessage": "Không thể kiểm tra proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Không thể phân giải Địa chỉ IP cho Máy chủ Proxy đã Định cấu hình {proxyHostName}", "ProxyPasswordHelpText": "Bạn chỉ cần nhập tên người dùng và mật khẩu nếu được yêu cầu. Nếu không, hãy để trống chúng.", "ProxyType": "Loại proxy", "ProxyUsernameHelpText": "Bạn chỉ cần nhập tên người dùng và mật khẩu nếu được yêu cầu. Nếu không, hãy để trống chúng.", @@ -60,7 +60,7 @@ "SSLPort": "Cổng SSL", "Style": "Phong cách", "SuggestTranslationChange": "Đề xuất thay đổi bản dịch", - "SystemTimeCheckMessage": "Thời gian hệ thống tắt hơn 1 ngày. Các tác vụ đã lên lịch có thể không chạy chính xác cho đến khi thời gian được sửa", + "SystemTimeHealthCheckMessage": "Thời gian hệ thống tắt hơn 1 ngày. Các tác vụ đã lên lịch có thể không chạy chính xác cho đến khi thời gian được sửa", "TableOptions": "Tùy chọn bảng", "TableOptionsColumnsMessage": "Chọn cột nào hiển thị và chúng xuất hiện theo thứ tự nào", "TagCannotBeDeletedWhileInUse": "Không thể bị xóa khi đang sử dụng", @@ -78,7 +78,7 @@ "Torrent": "Torrents", "UnableToAddANewIndexerPleaseTryAgain": "Không thể thêm trình chỉ mục mới, vui lòng thử lại.", "UpdateAutomaticallyHelpText": "Tự động tải xuống và cài đặt các bản cập nhật. Bạn vẫn có thể cài đặt từ Hệ thống: Cập nhật", - "UpdateCheckStartupNotWritableMessage": "Không thể cài đặt bản cập nhật vì người dùng '{0}' không thể ghi thư mục khởi động '{1}'.", + "UpdateStartupNotWritableHealthCheckMessage": "Không thể cài đặt bản cập nhật vì người dùng '{startupFolder}' không thể ghi thư mục khởi động '{userName}'.", "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", @@ -93,8 +93,8 @@ "Analytics": "phân tích", "Automatic": "Tự động", "DownloadClientSettings": "Tải xuống cài đặt ứng dụng khách", - "DownloadClientStatusCheckAllClientMessage": "Tất cả các ứng dụng khách tải xuống không khả dụng do lỗi", - "DownloadClientStatusCheckSingleClientMessage": "Ứng dụng khách tải xuống không khả dụng do lỗi: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "Tất cả các ứng dụng khách tải xuống không khả dụng do lỗi", + "DownloadClientStatusSingleClientHealthCheckMessage": "Ứng dụng khách tải xuống không khả dụng do lỗi: {downloadClientNames}", "Edit": "Biên tập", "EditIndexer": "Chỉnh sửa trình lập chỉ mục", "Enabled": "Đã bật", @@ -108,7 +108,7 @@ "RestoreBackup": "Khôi phục lại bản sao lưu", "Result": "Kết quả", "Retention": "Giữ lại", - "RSS": "RSS", + "Rss": "RSS", "SendAnonymousUsageData": "Gửi dữ liệu sử dụng ẩn danh", "Settings": "Cài đặt", "SettingsEnableColorImpairedModeHelpText": "Đã thay đổi kiểu để cho phép người dùng khiếm thị phân biệt rõ hơn thông tin mã màu", @@ -122,7 +122,7 @@ "Authentication": "Xác thực", "Backup": "Sao lưu", "DeleteTagMessageText": "Bạn có chắc chắn muốn xóa thẻ '{0}' không?", - "IndexerLongTermStatusCheckSingleClientMessage": "Trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ: {indexerNames}", "IndexerPriority": "Mức độ ưu tiên của người lập chỉ mục", "KeyboardShortcuts": "Các phím tắt bàn phím", "Language": "Ngôn ngữ", @@ -138,12 +138,12 @@ "Mechanism": "Cơ chế", "MIA": "MIA", "New": "Mới", - "ProxyCheckBadRequestMessage": "Không thể kiểm tra proxy. Mã trạng thái: {0}", + "ProxyBadRequestHealthCheckMessage": "Không thể kiểm tra proxy. Mã trạng thái: {statusCode}", "BackupRetentionHelpText": "Các bản sao lưu tự động cũ hơn khoảng thời gian lưu giữ sẽ tự động được dọn dẹp", "BindAddress": "Địa chỉ ràng buộc", "BranchUpdate": "Nhánh sử dụng để cập nhật {appName}", "CloseCurrentModal": "Đóng phương thức hiện tại", - "RSSIsNotSupportedWithThisIndexer": "RSS không được hỗ trợ với trình chỉ mục này", + "RssIsNotSupportedWithThisIndexer": "RSS không được hỗ trợ với trình chỉ mục này", "SettingsEnableColorImpairedMode": "Bật Chế độ Khuyết màu", "AnalyticsEnabledHelpText": "Gửi thông tin sử dụng và lỗi ẩn danh đến máy chủ của {appName}. Điều này bao gồm thông tin về trình duyệt của bạn, trang WebUI của {appName} mà bạn sử dụng, báo cáo lỗi cũng như hệ điều hành và phiên bản thời gian chạy. Chúng tôi sẽ sử dụng thông tin này để ưu tiên các tính năng và sửa lỗi.", "ApiKey": "Mã API", @@ -184,8 +184,8 @@ "NoChange": "Không thay đổi", "PortNumber": "Số cổng", "Proxy": "Ủy quyền", - "IndexerProxyStatusCheckAllClientMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi", - "IndexerStatusCheckAllClientMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi", "Info": "Thông tin", "MoreInfo": "Thêm thông tin", "MovieIndexScrollBottom": "Mục lục phim: Cuộn dưới cùng", @@ -214,8 +214,8 @@ "UnableToAddANewDownloadClientPleaseTryAgain": "Không thể thêm ứng dụng khách tải xuống mới, vui lòng thử lại.", "UnableToAddANewIndexerProxyPleaseTryAgain": "Không thể thêm trình chỉ mục mới, vui lòng thử lại.", "UnableToAddANewNotificationPleaseTryAgain": "Không thể thêm thông báo mới, vui lòng thử lại.", - "UnableToLoadBackups": "Không thể tải các bản sao lưu", - "UnableToLoadDownloadClients": "Không thể tải ứng dụng khách tải xuống", + "BackupsLoadError": "Không thể tải các bản sao lưu", + "DownloadClientsLoadError": "Không thể tải ứng dụng khách tải xuống", "UnableToLoadGeneralSettings": "Không thể tải Cài đặt chung", "UnableToLoadHistory": "Không thể tải lịch sử", "UnableToLoadNotifications": "Không thể tải thông báo", @@ -223,8 +223,8 @@ "UnableToLoadUISettings": "Không thể tải cài đặt giao diện người dùng", "UnsavedChanges": "Các thay đổi chưa được lưu", "UnselectAll": "Bỏ chọn tất cả", - "UpdateCheckStartupTranslocationMessage": "Không thể cài đặt bản cập nhật vì thư mục khởi động '{0}' nằm trong thư mục Chuyển vị ứng dụng.", - "UpdateCheckUINotWritableMessage": "Không thể cài đặt bản cập nhật vì thư mục giao diện người dùng '{0}' không thể ghi bởi người dùng '{1}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Không thể cài đặt bản cập nhật vì thư mục khởi động '{startupFolder}' nằm trong thư mục Chuyển vị ứng dụng.", + "UpdateUiNotWritableHealthCheckMessage": "Không thể cài đặt bản cập nhật vì thư mục giao diện người dùng '{uiFolder}' không thể ghi bởi người dùng '{userName}'.", "UseProxy": "Sử dụng Proxy", "Wiki": "Wiki", "YesCancel": "Có, Hủy bỏ", @@ -256,7 +256,7 @@ "Grabbed": "Nắm lấy", "Grabs": "Vồ lấy", "Health": "Sức khỏe", - "HealthNoIssues": "Không có vấn đề với cấu hình của bạn", + "NoIssuesWithYourConfiguration": "Không có vấn đề với cấu hình của bạn", "RestartRequiredHelpTextWarning": "Yêu cầu khởi động lại để có hiệu lực", "ShowSearch": "Hiển thị Tìm kiếm", "ShowSearchHelpText": "Hiển thị nút tìm kiếm khi di chuột", @@ -297,9 +297,9 @@ "GeneralSettings": "Cài đặt chung", "Indexer": "Người lập chỉ mục", "IndexerFlags": "Cờ chỉ mục", - "IndexerLongTermStatusCheckAllClientMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ", "IndexerPriorityHelpText": "Mức độ ưu tiên của người lập chỉ mục từ 1 (Cao nhất) đến 50 (Thấp nhất). Mặc định: 25.", - "IndexerStatusCheckSingleClientMessage": "Trình lập chỉ mục không khả dụng do lỗi: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Trình lập chỉ mục không khả dụng do lỗi: {indexerNames}", "Interval": "Khoảng thời gian", "RefreshMovie": "Làm mới phim", "System": "Hệ thống", @@ -329,7 +329,7 @@ "Queued": "Đã xếp hàng", "Remove": "Tẩy", "Replace": "Thay thế", - "TheLatestVersionIsAlreadyInstalled": "Phiên bản mới nhất của {appName} đã được cài đặt", + "OnLatestVersion": "Phiên bản mới nhất của {appName} đã được cài đặt", "ApplyChanges": "Áp dụng thay đổi", "ApplyTagsHelpTextAdd": "Thêm: Thêm thẻ vào danh sách thẻ hiện có", "ApplyTagsHelpTextHowToApplyApplications": "Cách áp dụng thẻ cho các phim đã chọn", @@ -348,12 +348,54 @@ "ConnectionLostReconnect": "Radarr sẽ cố gắng kết nối tự động hoặc bạn có thể nhấp vào tải lại bên dưới.", "WhatsNew": "Có gì mới?", "NotificationStatusAllClientHealthCheckMessage": "Tất cả danh sách không có sẵn do lỗi", - "NotificationStatusSingleClientHealthCheckMessage": "Danh sách không có sẵn do lỗi: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Danh sách không có sẵn do lỗi: {notificationNames}", "AuthBasic": "Cơ bản (Cửa sổ bật lên trình duyệt)", "AuthForm": "Biểu mẫu (Trang đăng nhập)", "DisabledForLocalAddresses": "Bị vô hiệu hóa đối với địa chỉ địa phương", "None": "không ai", "ResetAPIKeyMessageText": "Bạn có chắc chắn muốn đặt lại Khóa API của mình không?", "ApiKeyValidationHealthCheckMessage": "Hãy cập nhật mã API để dài ít nhất {length} kí tự. Bạn có thể làm điều này trong cài đặt hoặc trong tập config", - "RestartProwlarr": "Khởi động lại {appName}" + "RestartProwlarr": "Khởi động lại {appName}", + "CustomFilter": "Bộ lọc tùy chỉnh", + "IndexerHDBitsSettingsMediums": "Trung bình", + "ProxyValidationBadRequest": "Không thể kiểm tra proxy. Mã trạng thái: {statusCode}", + "GrabRelease": "Lấy bản phát hành", + "BuiltIn": "Được xây dựng trong", + "Script": "Kịch bản", + "PublishedDate": "Ngày xuất bản", + "AllSearchResultsHiddenByFilter": "Tất cả kết quả bị ẩn bởi bộ lọc được áp dụng", + "AptUpdater": "Sử dụng apt để cài đặt bản cập nhật", + "DockerUpdater": "cập nhật vùng chứa docker để nhận bản cập nhật", + "Download": "Tải xuống", + "NoEventsFound": "Không tìm thấy sự kiện", + "ErrorRestoringBackup": "Lỗi khi khôi phục bản sao lưu", + "ExternalUpdater": "{appName} được định cấu hình để sử dụng cơ chế cập nhật bên ngoài", + "RestartReloadNote": "Lưu ý: {appName} sẽ tự động khởi động lại và tải lại giao diện người dùng trong quá trình khôi phục.", + "UpdateAppDirectlyLoadError": "Không thể cập nhật {appName} trực tiếp,", + "InstallLatest": "Cài đặt mới nhất", + "AuthenticationMethodHelpTextWarning": "Vui lòng chọn một phương thức xác thực hợp lệ", + "AuthenticationRequiredPasswordHelpTextWarning": "Nhập mật khẩu mới", + "AuthenticationRequiredUsernameHelpTextWarning": "Nhập tên người dùng mới", + "UpdaterLogFiles": "Tệp nhật ký của trình cập nhật", + "UseSsl": "Dùng SSL", + "AppUpdatedVersion": "{appName} đã được cập nhật lên phiên bản `{version}`, để nhận được những thay đổi mới nhất, bạn cần tải lại {appName}", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Xác nhận mật khẩu mới", + "UpdateAvailableHealthCheckMessage": "Có cập nhật mới: {version}", + "AuthenticationRequiredWarning": "Để ngăn truy cập từ xa mà không cần xác thực, {appName} hiện yêu cầu bật xác thực. Bạn có thể tùy ý tắt xác thực từ các địa chỉ cục bộ.", + "AuthenticationRequired": "Bắt buộc phải xác thực", + "AuthenticationMethod": "Phương thức xác thực", + "AddDownloadClientImplementation": "Thêm trình tải xuống - {implementationName}", + "AddIndexerImplementation": "Thêm trình lập chỉ mục - {implementationName}", + "AddConnection": "Thêm kết nối", + "AddConnectionImplementation": "Thêm điều kiện - {implementationName}", + "AppUpdated": "{appName} đã cập nhật", + "Clone": "Đóng", + "EditIndexerImplementation": "Thêm điều kiện - {implementationName}", + "CurrentlyInstalled": "Mới cài đặt", + "EditApplicationImplementation": "Thêm điều kiện - {implementationName}", + "Stats": "Trạng thái", + "Season": "Lý do", + "Mixed": "đã sửa", + "EditConnectionImplementation": "Thêm điều kiện - {implementationName}", + "AddApplicationImplementation": "Thêm điều kiện - {implementationName}" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index adf690e4e..41f815655 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1,6 +1,6 @@ { "About": "关于", - "AcceptConfirmationModal": "接受确认模组Accept Confirmation Modal", + "AcceptConfirmationModal": "接受确认对话框", "Actions": "动作", "Add": "添加", "AddApplication": "添加应用程序", @@ -13,18 +13,18 @@ "AddRemoveOnly": "仅添加和删除", "AddSyncProfile": "添加同步配置文件", "AddToDownloadClient": "添加发布到下载客户端", - "Added": "已添加", + "Added": "添加日期", "AddedToDownloadClient": "发布已添加档案到客户端", "AddingTag": "添加标签", - "Age": "年龄", + "Age": "寿命", "All": "全部", "AllIndexersHiddenDueToFilter": "由于应用了筛选器,所有索引器都被隐藏。", "Analytics": "分析", - "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到{appName}的服务器。这包括有关您的浏览器的信息、您使用的{appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", - "ApiKey": "API Key", - "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{0}个字符长。您可以通过设置或配置文件执行此操作", - "AppDataDirectory": "AppData目录", - "AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除", + "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到 {appName} 的服务器。这包括有关您的浏览器信息、您使用的 {appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", + "ApiKey": "API 密钥", + "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少 {length} 个字符长。您可以通过设置或配置文件完成此操作", + "AppDataDirectory": "AppData 目录", + "AppDataLocationHealthCheckMessage": "为防止在更新时删除 AppData,更新将无法进行", "AppProfileInUse": "正在使用的应用程序配置文件", "AppProfileSelectHelpText": "应用程序配置用于控制应用程序同步设置 RSS、自动搜索和交互式搜索设置", "AppSettingsSummary": "配置{appName}与PVR程序交互方式的应用和设置", @@ -34,7 +34,7 @@ "ApplicationStatusCheckAllClientMessage": "由于故障所用应用程序都不可用", "ApplicationStatusCheckSingleClientMessage": "由于故障应用程序不可用", "ApplicationURL": "应用程序 URL", - "ApplicationUrlHelpText": "此应用的外部URL,包含 http(s)://、端口和基本URL", + "ApplicationUrlHelpText": "此应用的外部 URL,包含 http(s)://、端口和基本 URL", "Applications": "程序", "Apply": "应用", "ApplyTags": "应用标签", @@ -45,9 +45,9 @@ "Auth": "认证", "Authentication": "认证", "AuthenticationMethodHelpText": "需要用户名和密码以访问 {appName}", - "AuthenticationRequired": "需要身份验证", - "AuthenticationRequiredHelpText": "更改身份验证的请求。除非您了解风险,否则请勿更改。", - "AuthenticationRequiredWarning": "为了防止未经身份验证的远程访问,{appName} 现在需要启用身份验证。您可以禁用本地地址的身份验证。", + "AuthenticationRequired": "需要认证", + "AuthenticationRequiredHelpText": "修改这些请求需要认证。除非你了解其中的风险,否则不要进行更改。", + "AuthenticationRequiredWarning": "为防止未经认证的远程访问,{appName} 现需要启用身份认证。您可以选择禁用本地地址的身份认证。", "Author": "作者", "Automatic": "自动化", "AutomaticSearch": "自动搜索", @@ -89,10 +89,10 @@ "ConnectSettingsSummary": "通知和自定义脚本", "ConnectionLost": "连接丢失", "Connections": "连接", - "CouldNotConnectSignalR": "无法连接至SignalR,不会升级UI", + "CouldNotConnectSignalR": "无法连接至 SignalR,UI 将不会更新", "Custom": "自定义", "CustomFilters": "自定义过滤器", - "DBMigration": "数据库迁移版本", + "DatabaseMigration": "数据库迁移版本", "Database": "数据库", "Date": "日期", "Dates": "日期", @@ -101,14 +101,14 @@ "DeleteApplication": "删除应用程序", "DeleteApplicationMessageText": "您确定要删除应用程序“{name}”吗?", "DeleteBackup": "删除备份", - "DeleteBackupMessageText": "您确定要删除备份“{name}”吗?", + "DeleteBackupMessageText": "您确定要删除备份 “{name}” 吗?", "DeleteClientCategory": "删除下载客户端分类", "DeleteDownloadClient": "删除下载客户端", - "DeleteDownloadClientMessageText": "你确定要删除下载客户端 “{name}” 吗?", + "DeleteDownloadClientMessageText": "您确定要删除下载客户端 “{name}” 吗?", "DeleteIndexerProxy": "删除搜刮器代理", "DeleteIndexerProxyMessageText": "您确定要删除索引器代理“{name}”吗?", "DeleteNotification": "删除消息推送", - "DeleteNotificationMessageText": "您确定要删除通知“{name}”吗?", + "DeleteNotificationMessageText": "您确定要删除通知 “{name}” 吗?", "DeleteTag": "删除标签", "DeleteTagMessageText": "您确定要删除标签 '{label}' 吗?", "Description": "描述", @@ -116,14 +116,14 @@ "DevelopmentSettings": "开发设置", "Disabled": "禁用", "DisabledUntil": "禁用Until", - "Discord": "分歧", + "Discord": "Discord", "Docker": "Docker", "Donations": "赞助", "DownloadClient": "下载客户端", "DownloadClientCategory": "下载客户端分类", "DownloadClientSettings": "下载客户端设置", - "DownloadClientStatusCheckAllClientMessage": "所有下载客户端都不可用", - "DownloadClientStatusCheckSingleClientMessage": "所有下载客户端都不可用: {0}", + "DownloadClientStatusAllClientHealthCheckMessage": "所有下载客户端都不可用", + "DownloadClientStatusSingleClientHealthCheckMessage": "所有下载客户端都不可用: {downloadClientNames}", "DownloadClients": "下载客户端", "DownloadClientsSettingsSummary": "下载客户端配置以集成到 {appName} UI 搜索中", "Duration": "时长", @@ -133,13 +133,13 @@ "ElapsedTime": "运行时间", "Enable": "启用", "EnableAutomaticSearch": "启用自动搜索", - "EnableAutomaticSearchHelpText": "当自动搜索通过UI或{appName}执行时将被使用", + "EnableAutomaticSearchHelpText": "当自动搜索通过 UI 或 {appName} 执行时将被使用", "EnableIndexer": "启用搜刮器", "EnableInteractiveSearch": "启用手动搜索", "EnableInteractiveSearchHelpText": "当手动搜索启用时使用", "EnableRss": "启用RSS", "EnableRssHelpText": "为搜刮器启用 RSS订阅", - "EnableSSL": "启用SSL", + "EnableSSL": "启用 SSL", "EnableSslHelpText": " 重启生效", "Enabled": "已启用", "EnabledRedirected": "启用, 修改", @@ -173,7 +173,7 @@ "Grabbed": "已抓取", "Grabs": "抓取", "Health": "健康度", - "HealthNoIssues": "您的设置没有问题", + "NoIssuesWithYourConfiguration": "您的设置没有问题", "HideAdvanced": "隐藏高级设置", "History": "历史记录", "HistoryCleanup": "清理历史记录", @@ -194,29 +194,29 @@ "IndexerDetails": "‎索引器‎‎详细信息‎", "IndexerDisabled": "索引器已被禁用", "IndexerFailureRate": "Indexer失败率", - "IndexerFlags": "搜刮器标记", + "IndexerFlags": "索引器标志", "IndexerHealthCheckNoIndexers": "未启用任何搜刮器,{appName}将不会返回搜索结果", "IndexerInfo": "索引器信息", - "IndexerLongTermStatusCheckAllClientMessage": "由于故障超过6小时,所有搜刮器均不可用", - "IndexerLongTermStatusCheckSingleClientMessage": "由于故障6小时,下列搜刮器都已不可用:{0}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "由于故障超过6小时,所有搜刮器均不可用", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "由于故障6小时,下列搜刮器都已不可用:{indexerNames}", "IndexerName": "‎索引‎‎名字‎", - "IndexerNoDefCheckMessage": "索引器没有定义,将无法工作: {0}. 请删除或重新添加到{appName}", + "IndexerNoDefinitionCheckHealthCheckMessage": "索引器没有定义,将无法工作: {indexerNames}. 请删除或重新添加到{appName}", "IndexerObsoleteCheckMessage": "搜刮器已过弃用或已更新:{0}。请将其删除和(或)重新添加到 {appName}", "IndexerPriority": "搜刮器优先级", "IndexerPriorityHelpText": "索引器优先级从1(最高)到50(最低),默认25。", "IndexerProxies": "搜刮器代理", "IndexerProxy": "搜刮器代理", - "IndexerProxyStatusCheckAllClientMessage": "所有搜刮器都因错误不可用", - "IndexerProxyStatusCheckSingleClientMessage": "搜刮器因错误不可用:{0}", + "IndexerProxyStatusAllUnavailableHealthCheckMessage": "所有搜刮器都因错误不可用", + "IndexerProxyStatusUnavailableHealthCheckMessage": "搜刮器因错误不可用:{indexerProxyNames}", "IndexerQuery": "搜刮器查询", "IndexerRss": "搜刮器RSS", "IndexerSettingsSummary": "配置全局索引器设置,包括代理。", "IndexerSite": "‎索引‎‎网站‎", - "IndexerStatusCheckAllClientMessage": "所有搜刮器都因错误不可用", - "IndexerStatusCheckSingleClientMessage": "搜刮器因错误不可用:{0}", + "IndexerStatusAllUnavailableHealthCheckMessage": "所有搜刮器都因错误不可用", + "IndexerStatusUnavailableHealthCheckMessage": "搜刮器因错误不可用:{indexerNames}", "IndexerTagsHelpText": "使用标签来指定索引器代理或索引器同步到哪些应用程序。", - "IndexerVipCheckExpiredClientMessage": "索引器VIP特权已过期:{0}", - "IndexerVipCheckExpiringClientMessage": "索引器VIP特权即将过期:{0}", + "IndexerVipExpiredHealthCheckMessage": "索引器VIP特权已过期:{indexerNames}", + "IndexerVipExpiringHealthCheckMessage": "索引器VIP特权即将过期:{indexerNames}", "Indexers": "索引器", "Info": "信息", "InitialFailure": "初始化失败", @@ -307,10 +307,10 @@ "ProwlarrSupportsAnyIndexer": "{appName}支持多种搜刮器,包括任何使用Newznab/Torznab标准的搜刮器(“通用Newznab”对应Usenet,“Generic Torznab”对应Torrents)。从以下搜索并选择你的搜刮器。", "Proxies": "代理", "Proxy": "代理", - "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", - "ProxyCheckBadRequestMessage": "测试代理失败。状态码:{0}", - "ProxyCheckFailedToTestMessage": "测试代理失败: {0}", - "ProxyCheckResolveIpMessage": "无法解析已设置的代理服务器主机{0}的IP地址", + "ProxyBypassFilterHelpText": "使用 “ , ” 作为分隔符,并使用 “ *. ” 作为二级域名的通配符", + "ProxyBadRequestHealthCheckMessage": "测试代理失败。状态码:{statusCode}", + "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {url}", + "ProxyResolveIpHealthCheckMessage": "无法解析已设置的代理服务器主机{proxyHostName}的IP地址", "ProxyPasswordHelpText": "如果需要,您只需要输入用户名和密码,否则就让它们为空。", "ProxyType": "代理类型", "ProxyUsernameHelpText": "如果需要,您只需要输入用户名和密码。否则就让它们为空。", @@ -321,8 +321,8 @@ "QueryResults": "查询结果", "Queue": "队列", "Queued": "队列中", - "RSS": "RSS", - "RSSIsNotSupportedWithThisIndexer": "该搜刮器不支持RSS", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "该索引器不支持 RSS", "RawSearchSupported": "‎支持原始‎‎搜索‎", "ReadTheWikiForMoreInformation": "查阅Wiki获得更多信息", "Reddit": "Reddit", @@ -368,7 +368,7 @@ "Season": "季", "Security": "安全", "Seeders": "种子", - "SelectAll": "选择全部", + "SelectAll": "全选", "SemiPrivate": "‎半私有‎", "SendAnonymousUsageData": "发送匿名使用数据", "SetTags": "设置标签", @@ -389,13 +389,13 @@ "SettingsShowRelativeDatesHelpText": "显示相对日期(今天昨天等)或绝对日期", "SettingsSqlLoggingHelpText": "记录来自{appName}的所有SQL查询", "SettingsTimeFormat": "时间格式", - "ShowAdvanced": "显示高级设置", + "ShowAdvanced": "高级设置", "ShowSearch": "显示搜索", "ShowSearchHelpText": "悬停时显示搜索按钮", "Shutdown": "关机", "Size": "大小", "Sort": "排序", - "Source": "来源", + "Source": "代码", "StartTypingOrSelectAPathBelow": "输入路径或者从下面选择", "Started": "已开始", "StartupDirectory": "启动目录", @@ -411,7 +411,7 @@ "SyncProfile": "同步配置文件", "SyncProfiles": "同步配置文件", "System": "系统", - "SystemTimeCheckMessage": "系统时间相差超过1天。在纠正时间之前,计划的任务可能无法正确运行", + "SystemTimeHealthCheckMessage": "系统时间相差超过1天。在纠正时间之前,计划的任务可能无法正确运行", "TVSearchTypes": "‎电视‎‎搜索‎‎类型‎", "TableOptions": "表格选项", "TableOptionsColumnsMessage": "选择显示哪些列并排序", @@ -425,8 +425,8 @@ "TestAll": "测试全部", "TestAllApps": "测试全部应用", "TestAllClients": "测试全部客户端", - "TestAllIndexers": "测试全部搜刮器", - "TheLatestVersionIsAlreadyInstalled": "已安装最新版本的{appName}", + "TestAllIndexers": "测试全部索引器", + "OnLatestVersion": "已安装最新版本的{appName}", "Theme": "主题", "ThemeHelpText": "更改应用程序UI主题,“自动”主题将使用您的操作系统主题设置亮或暗模式。灵感来源于{inspirredby}。", "Time": "时间", @@ -451,10 +451,9 @@ "UnableToAddANewIndexerProxyPleaseTryAgain": "无法添加搜刮器,请稍后重试。", "UnableToAddANewNotificationPleaseTryAgain": "无法添加新通知,请稍后重试。", "UnableToLoadAppProfiles": "无法加载应用配置", - "UnableToLoadApplicationList": "123", - "UnableToLoadBackups": "无法加载备份", + "BackupsLoadError": "无法加载备份", "UnableToLoadDevelopmentSettings": "无法加载开发设置", - "UnableToLoadDownloadClients": "无法加载下载客户端", + "DownloadClientsLoadError": "无法加载下载客户端", "UnableToLoadGeneralSettings": "无法加载通用设置", "UnableToLoadHistory": "无法加载历史记录", "UnableToLoadIndexerProxies": "无法加载索引器代理", @@ -463,13 +462,13 @@ "UnableToLoadTags": "无法加载标签", "UnableToLoadUISettings": "无法加载UI设置", "UnsavedChanges": "未保存更改", - "UnselectAll": "取消选择全部", - "UpdateAutomaticallyHelpText": "自动下载并安装更新。你还可以在“系统:更新”中安装", - "UpdateAvailable": "有新的更新可用", - "UpdateCheckStartupNotWritableMessage": "无法安装更新,因为用户“{1}”对于启动文件夹“{0}”没有写入权限。", - "UpdateCheckStartupTranslocationMessage": "无法安装更新,因为启动文件夹“{0}”在一个应用程序迁移文件夹。Cannot install update because startup folder '{0}' is in an App Translocation folder.", - "UpdateCheckUINotWritableMessage": "无法安装升级,因为用户“{1}”不可写入界面文件夹“{0}”。", - "UpdateMechanismHelpText": "使用 {appName} 内置的更新器或者脚本", + "UnselectAll": "取消全选", + "UpdateAutomaticallyHelpText": "自动下载并安装更新。您还可以在「“系统”->“更新”」中安装", + "UpdateAvailableHealthCheckMessage": "有新的更新可用", + "UpdateStartupNotWritableHealthCheckMessage": "无法安装更新,因为用户“{userName}”对于启动文件夹“{startupFolder}”没有写入权限。", + "UpdateStartupTranslocationHealthCheckMessage": "无法安装更新,因为启动文件夹“{0}”在一个应用程序迁移文件夹。Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.", + "UpdateUiNotWritableHealthCheckMessage": "无法安装升级,因为用户“{userName}”不可写入界面文件夹“{uiFolder}”。", + "UpdateMechanismHelpText": "使用 {appName} 内置的更新程序或脚本", "UpdateScriptPathHelpText": "自定义脚本的路径,该脚本处理获取的更新包并处理更新过程的其余部分", "Updates": "更新", "Uptime": "运行时间", @@ -485,7 +484,7 @@ "Warn": "警告", "Website": "‎网站‎", "Wiki": "Wiki", - "Year": "年", + "Year": "年份", "Yes": "确定", "YesCancel": "确定,取消", "Yesterday": "昨天", @@ -494,9 +493,9 @@ "ApplyChanges": "应用更改", "ApplyTagsHelpTextAdd": "添加: 添加标签至已有的标签列表中", "CountDownloadClientsSelected": "已选择 {count} 个下载客户端", - "CountIndexersSelected": "已选择 {count} 个索引器", - "DeleteSelectedDownloadClientsMessageText": "您确定要删除{count}选定的下载客户端吗?", - "DeleteSelectedIndexersMessageText": "您确定要删除{count}选定的索引器吗?", + "CountIndexersSelected": "选定 {count} 个索引器", + "DeleteSelectedDownloadClientsMessageText": "您确定要删除 {count} 个选定的下载客户端吗?", + "DeleteSelectedIndexersMessageText": "您确定要删除选定的 {count} 个索引器吗?", "EditSelectedDownloadClients": "编辑选定的下载客户端", "Implementation": "执行", "ManageDownloadClients": "管理下载客户端", @@ -504,12 +503,12 @@ "NoIndexersFound": "未找到索引器", "SelectIndexers": "搜刮器搜索", "ApplyTagsHelpTextHowToApplyApplications": "如何给选中的电影添加标签", - "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器", + "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选中的索引器", "ApplyTagsHelpTextReplace": "替换: 用输入的标签替换当前标签 (不输入将会清除所有标签)", "DeleteSelectedApplicationsMessageText": "您确定要删除{count}选定的应用程序吗?", "DeleteSelectedDownloadClients": "删除下载客户端", "DownloadClientPriorityHelpText": "优先考虑多个下载客户端,循环查询用于具有相同优先级的客户端。", - "EditSelectedIndexers": "编辑选定的索引器", + "EditSelectedIndexers": "编辑选定索引器", "OnHealthRestored": "健康度恢复", "OnHealthRestoredHelpText": "健康度恢复", "ApplyTagsHelpTextRemove": "移除: 移除已输入的标签", @@ -560,11 +559,11 @@ "AddConnectionImplementation": "添加连接- {implementationName}", "AddDownloadClientImplementation": "添加下载客户端- {implementationName}", "AddIndexerProxyImplementation": "添加搜刮器代理-{实体名称}", - "AppUpdated": "{appName} 升级", + "AppUpdated": "{appName} 已升级", "EditDownloadClientImplementation": "编辑下载客户端- {implementationName}", "EditIndexerImplementation": "编辑索引器- {implementationName}", "EditIndexerProxyImplementation": "添加搜刮器代理-{实体名称}", - "AppUpdatedVersion": "{appName} 已经更新到 {version} 版本,重新加载 {appName} 使更新生效", + "AppUpdatedVersion": "{appName} 已经更新到版本 {version} ,重新加载 {appName} 使更新生效", "EditApplicationImplementation": "添加应用-{实体名称}", "EditConnectionImplementation": "编辑连接- {implementationName}", "NotificationStatusAllClientHealthCheckMessage": "由于故障所用应用程序都不可用", @@ -572,7 +571,7 @@ "AuthBasic": "基础(浏览器弹出对话框)", "AuthForm": "表单(登陆页面)", "AuthenticationMethod": "认证方式", - "AuthenticationMethodHelpTextWarning": "请选择一个有效的身份验证方式", + "AuthenticationMethodHelpTextWarning": "请选择一个有效的认证方式", "AuthenticationRequiredPasswordHelpTextWarning": "请输入新密码", "AuthenticationRequiredUsernameHelpTextWarning": "请输入新用户名", "Clone": "复制", @@ -581,7 +580,7 @@ "External": "外部的", "None": "无", "ResetAPIKeyMessageText": "您确定要重置您的 API 密钥吗?", - "IndexerDownloadClientHealthCheckMessage": "有无效下载客户端的索引器:{0}。", + "IndexerDownloadClientHealthCheckMessage": "使用无效下载客户端的索引器:{indexerNames}。", "ApplicationTagsHelpTextWarning": "标签应该谨慎使用,它们可能会产生意想不到的效果。带有标签的应用程序只会与具有相同标签的索引器同步。", "EditCategory": "编辑分类", "IndexerHistoryLoadError": "加载索引器历史记录出错", @@ -597,5 +596,163 @@ "ActiveIndexers": "活动索引器", "ActiveApps": "活动应用程序", "AppsMinimumSeeders": "应用程序最少种子数", - "PackSeedTime": "做种时间" + "PackSeedTime": "做种时间", + "PasswordConfirmation": "确认密码", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "确认新密码", + "InvalidUILanguage": "您的 UI 设置为无效语言,请纠正并保存设置", + "NoIndexerCategories": "没有找到此索引器的分类", + "DownloadClientQbittorrentSettingsContentLayout": "内容布局", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 配置的内容布局,使用种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)", + "DownloadClientAriaSettingsDirectoryHelpText": "下载位置可选择,留空使用 Aria2 默认位置", + "ManageClients": "管理客户端", + "CustomFilter": "自定义过滤器", + "BlackholeFolderHelpText": "{appName} 将在其中存储 {extension} 文件的文件夹", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashes": "在抓取时不会同步黑名单种子", + "UsenetBlackholeNzbFolder": "NZB文件夹", + "DownloadClientFreeboxSettingsAppIdHelpText": "创建访问 Freebox API 所需的 App ID(即 “app_id”)", + "DownloadClientNzbgetSettingsAddPausedHelpText": "此选项至少需要 NzbGet 版本 16.0", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "按顺序下载文件(qBittorrent 4.1.0+)", + "TorrentBlackholeSaveMagnetFilesHelpText": "如果没有可用的 .torrent 文件,请保存磁力链接(仅当下载客户端支持保存到文件的磁力连接时才有用)", + "DownloadClientFloodSettingsAdditionalTags": "附加标签", + "DownloadClientFreeboxSettingsApiUrl": "API 地址", + "DownloadClientFreeboxSettingsAppId": "App ID", + "DownloadClientDelugeSettingsUrlBaseHelpText": "向 Deluge JSON URL 添加前缀,请参阅 {url}", + "DownloadClientFloodSettingsUrlBaseHelpText": "为 Flood API 添加前缀,例如 {url}", + "DownloadClientFreeboxSettingsAppToken": "App Token", + "DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "添加到 qBittorrent 的种子的初始状态。 请注意,强制做种不遵守种子限制", + "GrabRelease": "抓取版本", + "ManualGrab": "手动抓取", + "OverrideAndAddToDownloadClient": "覆盖并添加到下载队列", + "OverrideGrabModalTitle": "覆盖并抓取 - {title}", + "PrioritySettings": "优先级: {priority}", + "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载客户端", + "Donate": "捐赠", + "DownloadClientPneumaticSettingsNzbFolder": "NZB文件夹", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "需要从 XBMC 访问此文件夹", + "DownloadClientQbittorrentSettingsSequentialOrder": "按顺序下载", + "DownloadClientRTorrentSettingsAddStopped": "添加后暂停", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "启用将在停止状态下向 rTorrent 添加 torrent 和磁力链接。 这可能会破坏磁力文件。", + "DownloadClientRTorrentSettingsDirectoryHelpText": "用于放置下载的可选位置,留空以使用默认的 rTorrent 位置", + "DownloadClientRTorrentSettingsUrlPath": "URL 地址", + "IndexerSettingsSeedRatio": "做种比率", + "SecretToken": "密钥令牌", + "XmlRpcPath": "XML RPC 路径", + "ApplicationSettingsSyncRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "用于存放下载内容的共享文件夹可选择,留空使用默认的 Download Station 位置", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "添加媒体属性作为标签。 提示是示例。", + "DownloadClientFreeboxSettingsApiUrlHelpText": "使用 API 版本定义 Freebox API 基本 URL,例如 “{url}”,默认为 “{defaultApiUrl}”", + "DownloadClientPneumaticSettingsStrmFolder": "Strm 文件夹", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "首先下载第一个和最后一个片段(qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "使用安全连接。 请参阅 qBittorrent 中的「选项 -> Web UI -> “使用 HTTPS 而不是 HTTP”」。", + "DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC 端点的路径,请参阅 {url}。 使用 ruTorrent 时,这通常是 RPC2 或 [ruTorrent 路径]{url2}。", + "DownloadClientSettingsDestinationHelpText": "手动指定下载目录,留空使用默认值", + "DownloadClientSettingsInitialState": "初始状态", + "DownloadClientSettingsInitialStateHelpText": "添加到 {clientName} 的种子初始状态", + "DownloadClientTransmissionSettingsDirectoryHelpText": "可选的下载位置,留空以使用默认传输位置", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "向 {clientName} RPC URL 添加前缀,例如 {url},默认为 '{defaultUrl}'", + "IndexerSettingsAppsMinimumSeeders": "应用程序最少种子数", + "IndexerSettingsPackSeedTimeIndexerHelpText": "种子下载的时间(季或专辑)应在停止前保持上传状态,应用程序默认设定为empty", + "IndexerHDBitsSettingsOriginsHelpText": "如果未指定,则使用所有选项。", + "DownloadClientFreeboxSettingsHostHelpText": "Freebox 的主机名或主机 IP 地址,默认为 “{url}”(仅在同一网络上有效)", + "DownloadClientFreeboxSettingsPortHelpText": "用于访问 Freebox 接口的端口,默认为 '{port}'", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "先下载首尾文件块", + "DownloadClientSettingsAddPaused": "添加并暂停", + "DownloadClientSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}", + "DownloadClientSettingsUseSslHelpText": "连接到 {clientName} 时使用安全连接", + "IndexerBeyondHDSettingsSearchTypes": "搜索类型", + "IndexerSettingsApiPath": "API 路径", + "IndexerSettingsApiPathHelpText": "API 的路径,通常是 {url}", + "IndexerSettingsAppsMinimumSeedersHelpText": "使用索引器抓去所需的最低种子数,如果为空,则同步配置文件的默认值", + "IndexerSettingsAdditionalParameters": "附加参数", + "IndexerSettingsSeedTime": "做种时间", + "IndexerSettingsVipExpiration": "VIP过期", + "Menu": "菜单", + "Mixed": "混合", + "TorrentBlackholeSaveMagnetFilesExtension": "保存磁力链接文件扩展名", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "用于磁力链接的扩展名,默认为 “.magnet”", + "Destination": "目标", + "Directory": "目录", + "IndexerHDBitsSettingsCodecs": "编解码器", + "IndexerHDBitsSettingsMediums": "媒介", + "IndexerHDBitsSettingsMediumsHelpText": "如果未指定,则使用所有选项。", + "IndexerSettingsPackSeedTime": "做种时间", + "IndexerSettingsCookie": "Cookie", + "ProxyValidationBadRequest": "测试代理失败。状态码:{statusCode}", + "ProxyValidationUnableToConnect": "无法连接到索引器:{exceptionMessage}。 检查有关此错误的日志以了解详细信息", + "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。下载必须具有所有初始标签才可被识别。 这可以避免与不相关的下载发生冲突。", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "该文件夹中的 .strm 文件将由 drone 导入", + "IndexerHDBitsSettingsCodecsHelpText": "如果未指定,则使用所有选项。", + "IndexerSettingsSeedRatioHelpText": "停止之前应达到的做种比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", + "IndexerSettingsSeedTimeHelpText": "停止前应做种的时间,留空使用下载客户端的默认值", + "TorrentBlackholeSaveMagnetFiles": "保存磁力链接文件", + "TorrentBlackholeTorrentFolder": "种子文件夹", + "UseSsl": "使用 SSL", + "Default": "默认", + "Any": "任何", + "BuiltIn": "内置的", + "Script": "脚本", + "InfoUrl": "信息 URL", + "PublishedDate": "发布日期", + "Redirected": "重定向", + "AllSearchResultsHiddenByFilter": "根据过滤条件所有结果已隐藏", + "HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查[日志]({link})来查找有关这些运行状况检查消息原因的更多信息。如果你在理解这些信息方面有困难,你可以通过下面的链接联系我们的支持。", + "PackageVersionInfo": "{packageVersion} 由 {packageAuthor} 制作", + "LabelIsRequired": "需要标签", + "LogSizeLimit": "日志大小限制", + "LogSizeLimitHelpText": "存档前的最大日志文件大小(MB)。默认值为 1 MB。", + "NotificationsTelegramSettingsIncludeAppName": "标题中包含 {appName}", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "可选,在消息标题前加上 {appName} 以区分来自不同应用的通知", + "NotificationsEmailSettingsUseEncryption": "启用加密", + "NotificationsEmailSettingsUseEncryptionHelpText": "是否优先使用加密(如果服务器已配置),始终使用通过SSL(仅端口465)或StartTLS(任何其他端口)进行加密,或从不使用加密", + "ClickToChangeQueryOptions": "单击以更改查询选项", + "ApplicationsLoadError": "无法加载应用程序列表", + "DownloadClientSettingsDefaultCategoryHelpText": "默认的备用分类,当发布资源没有匹配的分类时将使用此分类。为 {appName} 添加一个特定的分类,可以避免与非 {appName} 的无关下载发生冲突。分类是可选的,但强烈建议使用。", + "AverageGrabs": "平均抓取次数", + "AverageQueries": "平均查询次数", + "DefaultCategory": "默认分类", + "DownloadClientSettingsDefaultCategorySubFolderHelpText": "默认的备用分类,当发布资源没有匹配的分类时将使用此分类。为 {appName} 添加一个特定的分类,可以避免与非 {appName} 的无关下载发生冲突。分类是可选的,但强烈建议使用。启用分类后,会在输出目录中创建一个 [分类] 子目录。", + "ErrorRestoringBackup": "恢复备份错误", + "UpdaterLogFiles": "更新器日志文件", + "WouldYouLikeToRestoreBackup": "是否要还原备份 “{name}”?", + "AptUpdater": "使用apt安装更新", + "DockerUpdater": "更新Docker容器以更新应用", + "Download": "下载", + "ExternalUpdater": "{appName}配置为使用外部更新机制", + "FailedToFetchUpdates": "获取更新失败", + "LogFilesLocation": "日志文件位于: {location}", + "Logout": "注销", + "NoEventsFound": "无事件", + "RestartReloadNote": "注意:{appName}将在恢复过程中自动重启并重新加载UI。", + "TheLogLevelDefault": "默认的日志等级为 \"Info\",可以在 [常规设置] 中修改 (/settings/general)", + "UpdateAppDirectlyLoadError": "无法直接更新{appName},", + "InstallLatest": "安装最新版", + "Install": "安装", + "InstallMajorVersionUpdate": "安装更新", + "InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?", + "InstallMajorVersionUpdateMessageLink": "请查看 [{domain}]({url}) 以获取更多信息。", + "CurrentlyInstalled": "已安装", + "PreviouslyInstalled": "上次安装", + "FailedToFetchSettings": "设置同步失败", + "DownloadClientSettingsPriorityItemHelpText": "抓取内容时优先使用", + "IndexerAlphaRatioSettingsExcludeScene": "排除场景", + "IndexerAlphaRatioSettingsExcludeSceneHelpText": "从结果中排除场景版本", + "IndexerAlphaRatioSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerBeyondHDSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerBeyondHDSettingsLimitedOnly": "仅限", + "IndexerBeyondHDSettingsApiKeyHelpText": "来自网站的API密钥(在我的安全 => API密钥)", + "IndexerAvistazSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerAvistazSettingsPasswordHelpText": "网站密码", + "IndexerAvistazSettingsPidHelpText": "我的帐户或个人资料页的PID", + "IndexerAvistazSettingsUsernameHelpText": "网站用户名", + "IndexerAvistazSettingsUsernameHelpTextWarning": "只有成员级别及以上才能使用此索引器上的API。", + "IndexerBeyondHDSettingsLimitedOnlyHelpText": "仅限免费搜索(有限UL)", + "IndexerGazelleGamesSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerIPTorrentsSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerHDBitsSettingsUsernameHelpText": "网站用户名", + "IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerFileListSettingsFreeleechOnlyHelpText": "只搜索免费发布", + "IndexerFileListSettingsUsernameHelpText": "网站用户名", + "IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_Hans.json b/src/NzbDrone.Core/Localization/Core/zh_Hans.json new file mode 100644 index 000000000..54e88a350 --- /dev/null +++ b/src/NzbDrone.Core/Localization/Core/zh_Hans.json @@ -0,0 +1,7 @@ +{ + "About": "关于", + "Add": "添加", + "Analytics": "分析", + "Username": "用户名", + "AcceptConfirmationModal": "中文" +} diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index 1072856aa..0d1a65581 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -2,17 +2,17 @@ "About": "關於", "Add": "新增", "Added": "已新增", - "Actions": "執行", + "Actions": "動作", "Age": "年齡", "AddIndexer": "新增索引", "AddNewIndexer": "新增新索引", - "AddDownloadClient": "新增下載器", + "AddDownloadClient": "加入下載用戶端", "Analytics": "分析", "AddIndexerProxy": "新增索引器代理", "AddingTag": "新增標籤", "All": "全部", "AddRemoveOnly": "僅限新增或移除", - "AcceptConfirmationModal": "接受確認模式", + "AcceptConfirmationModal": "接受確認對話框", "Language": "語言", "Filter": "篩選", "Reload": "重新載入", @@ -70,10 +70,10 @@ "BranchUpdateMechanism": "外部更新機制使用的分支", "Publisher": "發布者", "Remove": "移除", - "RSS": "RSS", + "Rss": "RSS", "Season": "季", "Theme": "主題", - "ApiKeyValidationHealthCheckMessage": "請將您的API金鑰更新為至少{0}個字元長。您可以通過設定或配置文件進行此操作。", + "ApiKeyValidationHealthCheckMessage": "請將您的API金鑰更新為至少{length}個字元長。您可以通過設定或配置文件進行此操作。", "AppDataLocationHealthCheckMessage": "為了避免在更新過程中刪除AppData,將無法進行更新。", "AuthenticationMethodHelpText": "需要使用者名稱和密碼來存取Radarr", "Backup": "備份", @@ -92,7 +92,7 @@ "Queued": "佇列", "Replace": "替換", "Scheduled": "已排程", - "UI": "UI", + "UI": "使用者介面", "Apply": "套用", "Connections": "連接", "Reset": "重置", @@ -104,5 +104,65 @@ "AppDataDirectory": "AppData 路徑", "Applications": "應用程式", "AuthBasic": "基礎(瀏覽器彈出視窗)", - "AuthForm": "表單(登入頁面)" + "AuthForm": "表單(登入頁面)", + "AddConnection": "新增連接", + "AddConnectionImplementation": "新增連接 - {implementationName}", + "AddDownloadClientImplementation": "新增下載用戶端 - {implementationName}", + "AddIndexerImplementation": "新增索引 - {implementationName}", + "UnableToAddANewAppProfilePleaseTryAgain": "無法加入新的條件,請重新嘗試。", + "UnableToAddANewApplicationPleaseTryAgain": "無法加入新的條件,請重新嘗試。", + "UnableToAddANewDownloadClientPleaseTryAgain": "無法加入新的條件,請重新嘗試。", + "UnableToAddANewIndexerPleaseTryAgain": "無法加入新的索引器,請重新嘗試。", + "UnableToAddANewIndexerProxyPleaseTryAgain": "無法加入新的條件,請重新嘗試。", + "AddApplicationImplementation": "新增連接 - {implementationName}", + "AddIndexerProxyImplementation": "新增索引 - {implementationName}", + "EditConnectionImplementation": "新增連接 - {implementationName}", + "EditDownloadClientImplementation": "新增下載用戶端 - {implementationName}", + "UnableToAddANewNotificationPleaseTryAgain": "無法加入新的條件,請重新嘗試。", + "EditIndexerImplementation": "新增索引 - {implementationName}", + "EditApplicationImplementation": "新增連接 - {implementationName}", + "EditIndexerProxyImplementation": "新增索引 - {implementationName}", + "ApplyTagsHelpTextAdd": "加入:將標籤加入已存在的標籤清單", + "AppUpdated": "{appName}已更新", + "AppUpdatedVersion": "重新載入", + "ApplyChanges": "應用", + "ApplyTagsHelpTextHowToApplyApplications": "如何套用標籤在所選擇的輸入清單", + "Artist": "演員", + "Id": "ID", + "Usenet": "Usenet", + "ApplyTagsHelpTextHowToApplyIndexers": "如何套用標籤在所選擇的輸入清單", + "Docker": "Docker", + "IndexerHDBitsSettingsCodecs": "編解碼器", + "Directory": "目錄", + "BuiltIn": "內建的", + "AllSearchResultsHiddenByFilter": "根據所使用的篩選器已將所有結果隱藏", + "AptUpdater": "使用apt安裝更新", + "Discord": "Discord", + "Any": "任何", + "UpdateAppDirectlyLoadError": "無法直接更新 {appName},", + "UnselectAll": "取消全選", + "Uptime": "上線時間", + "AdvancedSettingsHiddenClickToShow": "進階設定已隱藏,點擊以顯示", + "UnableToLoadIndexers": "無法載入索引器", + "UnableToLoadHistory": "無法載入歷史記錄", + "UnableToLoadTags": "無法載入標籤", + "UILanguage": "使用者介面語言", + "UILanguageHelpText": "{appName} 介面所使用的語言", + "UISettings": "使用者介面設定", + "UnableToLoadUISettings": "無法載入 UI 設定", + "AddCustomFilter": "新增自定義過濾器", + "AddApplication": "新增應用程式", + "AddDownloadClientToProwlarr": "新增一個下載客戶端以允許 {appName} 從 UI 直接傳送手動搜尋結果。", + "AddCategory": "新增類別", + "AdvancedSettingsShownClickToHide": "進階設定已顯示,點擊以隱藏", + "Version": "版本", + "UpdateAvailableHealthCheckMessage": "可用的新版本: {version}", + "AuthenticationRequiredHelpText": "更改需要進行驗證的請求。除非你了解其中的風險,否則請勿修改。", + "AuthenticationRequiredPasswordHelpTextWarning": "請輸入新密碼", + "AuthenticationRequiredUsernameHelpTextWarning": "請輸入新用戶名", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "確認新密碼", + "AuthenticationRequired": "需要驗證", + "AuthenticationRequiredWarning": "為防止未經認證的遠程訪問,{appName} 現需要啟用身份認證。您可以選擇禁用本地地址的身份認證。", + "AuthenticationMethodHelpTextWarning": "請選擇一個有效的驗證方式", + "AuthenticationMethod": "驗證方式" } diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 020b5ec64..224173437 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Messaging.Commands } public virtual bool UpdateScheduledTask => true; - public virtual string CompletionMessage => "Completed"; + public virtual string CompletionMessage => null; public virtual bool RequiresDiskAccess => false; public virtual bool IsExclusive => false; public virtual bool IsTypeExclusive => false; diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index 12ce4cb06..6ad659294 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -105,6 +105,8 @@ namespace NzbDrone.Core.Messaging.Commands _logger.Trace("Publishing {0}", command.Name); _logger.Trace("Checking if command is queued or started: {0}", command.Name); + command.Trigger = trigger; + lock (_commandQueue) { var existingCommands = QueuedOrStarted(command.Name); @@ -141,7 +143,6 @@ namespace NzbDrone.Core.Messaging.Commands var command = GetCommand(commandName); command.LastExecutionTime = lastExecutionTime; command.LastStartTime = lastStartTime; - command.Trigger = trigger; return Push(command, priority, trigger); } @@ -232,13 +233,13 @@ namespace NzbDrone.Core.Messaging.Commands _repo.Trim(); } - private dynamic GetCommand(string commandName) + private Command GetCommand(string commandName) { commandName = commandName.Split('.').Last(); var commands = _knownTypes.GetImplementations(typeof(Command)); var commandType = commands.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)); - return Json.Deserialize("{}", commandType); + return Json.Deserialize("{}", commandType) as Command; } private void Update(CommandModel command, CommandStatus status, string message) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index f87917468..04bac39f0 100755 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; @@ -38,7 +39,31 @@ namespace NzbDrone.Core.Notifications.CustomScript public override string Link => "https://wiki.servarr.com/prowlarr/settings#connections"; - public override ProviderMessage Message => new ProviderMessage("Testing will execute the script with the EventType set to Test, ensure your script handles this correctly", ProviderMessageType.Warning); + public override ProviderMessage Message => new ("Testing will execute the script with the EventType set to Test, ensure your script handles this correctly", ProviderMessageType.Warning); + + public override void OnGrab(GrabMessage message) + { + var environmentVariables = new StringDictionary(); + + environmentVariables.Add("Prowlarr_EventType", "Grab"); + environmentVariables.Add("Prowlarr_InstanceName", _configFileProvider.InstanceName); + environmentVariables.Add("Prowlarr_ApplicationUrl", _configService.ApplicationUrl); + environmentVariables.Add("Prowlarr_Release_Title", message.Release.Title); + environmentVariables.Add("Prowlarr_Release_Indexer", message.Release.Indexer ?? string.Empty); + environmentVariables.Add("Prowlarr_Release_Size", message.Release.Size.ToString()); + environmentVariables.Add("Prowlarr_Release_Genres", string.Join("|", message.Release.Genres)); + environmentVariables.Add("Prowlarr_Release_Categories", string.Join("|", message.Release.Categories.Select(f => f.Name))); + environmentVariables.Add("Prowlarr_Release_IndexerFlags", string.Join("|", message.Release.IndexerFlags.Select(f => f.Name))); + environmentVariables.Add("Prowlarr_Release_PublishDate", message.Release.PublishDate.ToUniversalTime().ToString("s") + "Z"); + environmentVariables.Add("Prowlarr_Download_Client", message.DownloadClientName ?? string.Empty); + environmentVariables.Add("Prowlarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); + environmentVariables.Add("Prowlarr_Download_Id", message.DownloadId ?? string.Empty); + environmentVariables.Add("Prowlarr_Source", message.Source ?? string.Empty); + environmentVariables.Add("Prowlarr_Host", message.Host ?? string.Empty); + environmentVariables.Add("Prowlarr_Redirect", message.Redirect.ToString()); + + ExecuteScript(environmentVariables); + } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { @@ -93,14 +118,6 @@ namespace NzbDrone.Core.Notifications.CustomScript failures.Add(new NzbDroneValidationFailure("Path", "File does not exist")); } - foreach (var systemFolder in SystemFolders.GetSystemFolders()) - { - if (systemFolder.IsParentPath(Settings.Path)) - { - failures.Add(new NzbDroneValidationFailure("Path", $"Must not be a descendant of '{systemFolder}'")); - } - } - if (failures.Empty()) { try @@ -138,10 +155,5 @@ namespace NzbDrone.Core.Notifications.CustomScript return processOutput; } - - private bool ValidatePathParent(string possibleParent, string path) - { - return possibleParent.IsParentPath(path); - } } } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs index f4d4d7803..ce5a398ed 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications.CustomScript public CustomScriptSettingsValidator() { RuleFor(c => c.Path).IsValidPath(); + RuleFor(c => c.Path).SetValidator(new SystemFolderValidator()).WithMessage("Must not be a descendant of '{systemFolder}'"); RuleFor(c => c.Arguments).Empty().WithMessage("Arguments are no longer supported for custom scripts"); } } diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 9eb41e989..48b93f35d 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Notifications.Discord.Payloads; using NzbDrone.Core.Validation; @@ -10,10 +11,12 @@ namespace NzbDrone.Core.Notifications.Discord public class Discord : NotificationBase<DiscordSettings> { private readonly IDiscordProxy _proxy; + private readonly IConfigFileProvider _configFileProvider; - public Discord(IDiscordProxy proxy) + public Discord(IDiscordProxy proxy, IConfigFileProvider configFileProvider) { _proxy = proxy; + _configFileProvider = configFileProvider; } public override string Name => "Discord"; @@ -22,18 +25,18 @@ namespace NzbDrone.Core.Notifications.Discord public override void OnGrab(GrabMessage message) { var embed = new Embed - { - Author = new DiscordAuthor - { - Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, - IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" - }, - Title = RELEASE_GRABBED_TITLE, - Description = message.Message, - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - Color = message.Successful ? (int)DiscordColors.Success : (int)DiscordColors.Danger, - Fields = new List<DiscordField>() - }; + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? _configFileProvider.InstanceName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" + }, + Title = RELEASE_GRABBED_TITLE, + Description = message.Message, + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Color = message.Successful ? (int)DiscordColors.Success : (int)DiscordColors.Danger, + Fields = new List<DiscordField>() + }; foreach (var field in Settings.GrabFields) { @@ -80,81 +83,72 @@ namespace NzbDrone.Core.Notifications.Discord public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { - var attachments = new List<Embed> - { - new Embed - { - Author = new DiscordAuthor - { - Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, - IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" - }, - Title = healthCheck.Source.Name, - Description = healthCheck.Message, - Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Danger - } - }; + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? _configFileProvider.InstanceName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" + }, + Title = healthCheck.Source.Name, + Description = healthCheck.Message, + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Danger + }; - var payload = CreatePayload(null, attachments); + var payload = CreatePayload(null, new List<Embed> { embed }); _proxy.SendPayload(payload, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - var attachments = new List<Embed> - { - new Embed - { - Author = new DiscordAuthor - { - Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, - IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" - }, - 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"), - Color = (int)DiscordColors.Success - } - }; + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? _configFileProvider.InstanceName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png" + }, + 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"), + Color = (int)DiscordColors.Success + }; - var payload = CreatePayload(null, attachments); + var payload = CreatePayload(null, new List<Embed> { embed }); _proxy.SendPayload(payload, Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { - var attachments = new List<Embed> - { - new Embed - { - Author = new DiscordAuthor - { - Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, - 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"), - Color = (int)DiscordColors.Standard, - Fields = new List<DiscordField>() - { - new DiscordField() - { - Name = "Previous Version", - Value = updateMessage.PreviousVersion.ToString() - }, - new DiscordField() - { - Name = "New Version", - Value = updateMessage.NewVersion.ToString() - } - }, - } - }; + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? _configFileProvider.InstanceName : Settings.Author, + 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"), + Color = (int)DiscordColors.Standard, + Fields = new List<DiscordField> + { + new () + { + Name = "Previous Version", + Value = updateMessage.PreviousVersion.ToString() + }, + new () + { + Name = "New Version", + Value = updateMessage.NewVersion.ToString() + } + }, + }; - var payload = CreatePayload(null, attachments); + var payload = CreatePayload(null, new List<Embed> { embed }); _proxy.SendPayload(payload, Settings); } @@ -208,19 +202,5 @@ namespace NzbDrone.Core.Notifications.Discord return payload; } - - private static string BytesToString(long byteCount) - { - string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB - if (byteCount == 0) - { - return "0 " + suf[0]; - } - - var bytes = Math.Abs(byteCount); - var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); - var num = Math.Round(bytes / Math.Pow(1024, place), 1); - return string.Format("{0} {1}", (Math.Sign(byteCount) * num).ToString(), suf[place]); - } } } diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs index 45aedf189..c571b4fb5 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs @@ -19,10 +19,18 @@ namespace NzbDrone.Core.Notifications.Discord public DiscordSettings() { //Set Default Fields - GrabFields = new List<int> { 0, 1, 2, 3, 5, 6, 7, 8, 9 }; + GrabFields = new List<int> + { + (int)DiscordGrabFieldType.Release, + (int)DiscordGrabFieldType.Indexer, + (int)DiscordGrabFieldType.DownloadClient, + (int)DiscordGrabFieldType.GrabTrigger, + (int)DiscordGrabFieldType.Source, + (int)DiscordGrabFieldType.Host + }; } - private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator(); + private static readonly DiscordSettingsValidator Validator = new (); [FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")] public string WebHookUrl { get; set; } @@ -36,7 +44,7 @@ namespace NzbDrone.Core.Notifications.Discord [FieldDefinition(3, Label = "Host", Advanced = true, HelpText = "Override the Host that shows for this notification, Blank is machine name", Type = FieldType.Textbox)] public string Author { get; set; } - [FieldDefinition(4, Label = "On Grab Fields", Advanced = true, SelectOptions = typeof(DiscordGrabFieldType), HelpText = "Change the fields that are passed in for this 'on grab' notification", Type = FieldType.TagSelect)] + [FieldDefinition(4, Label = "On Grab Fields", Advanced = true, SelectOptions = typeof(DiscordGrabFieldType), HelpText = "Change the fields that are passed in for this 'on grab' notification", Type = FieldType.Select)] public IEnumerable<int> GrabFields { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index 6f09eec92..45719839e 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -108,47 +108,42 @@ namespace NzbDrone.Core.Notifications.Email private void Send(MimeMessage email, EmailSettings settings) { - using (var client = new SmtpClient()) + using var client = new SmtpClient(); + client.Timeout = 10000; + + var useEncyption = (EmailEncryptionType)settings.UseEncryption; + + var serverOption = useEncyption switch { - client.Timeout = 10000; + EmailEncryptionType.Always => settings.Port == 465 + ? SecureSocketOptions.SslOnConnect + : SecureSocketOptions.StartTls, + EmailEncryptionType.Never => SecureSocketOptions.None, + _ => SecureSocketOptions.Auto + }; - var serverOption = SecureSocketOptions.Auto; + client.ServerCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError; - if (settings.RequireEncryption) - { - if (settings.Port == 465) - { - serverOption = SecureSocketOptions.SslOnConnect; - } - else - { - serverOption = SecureSocketOptions.StartTls; - } - } + _logger.Debug("Connecting to mail server"); - client.ServerCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError; + client.Connect(settings.Server, settings.Port, serverOption); - _logger.Debug("Connecting to mail server"); + if (!string.IsNullOrWhiteSpace(settings.Username)) + { + _logger.Debug("Authenticating to mail server"); - client.Connect(settings.Server, settings.Port, serverOption); - - if (!string.IsNullOrWhiteSpace(settings.Username)) - { - _logger.Debug("Authenticating to mail server"); - - client.Authenticate(settings.Username, settings.Password); - } - - _logger.Debug("Sending to mail server"); - - client.Send(email); - - _logger.Debug("Sent to mail server, disconnecting"); - - client.Disconnect(true); - - _logger.Debug("Disconnecting from mail server"); + client.Authenticate(settings.Username, settings.Password); } + + _logger.Debug("Sending to mail server"); + + client.Send(email); + + _logger.Debug("Sent to mail server, disconnecting"); + + client.Disconnect(true); + + _logger.Debug("Disconnecting from mail server"); } private MailboxAddress ParseAddress(string type, string address) diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index ad386b57a..2ef1d9d0c 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -44,8 +44,8 @@ namespace NzbDrone.Core.Notifications.Email [FieldDefinition(1, Label = "Port")] public int Port { get; set; } - [FieldDefinition(2, Label = "Require Encryption", HelpText = "Require SSL (Port 465 only) or StartTLS (any other port)", Type = FieldType.Checkbox)] - public bool RequireEncryption { get; set; } + [FieldDefinition(2, Label = "NotificationsEmailSettingsUseEncryption", HelpText = "NotificationsEmailSettingsUseEncryptionHelpText", Type = FieldType.Select, SelectOptions = typeof(EmailEncryptionType))] + public int UseEncryption { get; set; } [FieldDefinition(3, Label = "Username", HelpText = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] public string Username { get; set; } @@ -70,4 +70,11 @@ namespace NzbDrone.Core.Notifications.Email return new NzbDroneValidationResult(Validator.Validate(this)); } } + + public enum EmailEncryptionType + { + Preferred = 0, + Always = 1, + Never = 2 + } } diff --git a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs index 5ee380e8b..54380dae3 100644 --- a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs +++ b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Notifications.Gotify public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", null); + _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index f9d79940e..824aae0b5 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Notifications foreach (var notification in notifications) { - if (blockedNotifications.TryGetValue(notification.Definition.Id, out var notificationStatus)) + if (blockedNotifications.TryGetValue(notification.Definition.Id, out var notificationStatus) && notificationStatus.DisabledTill.HasValue) { _logger.Debug("Temporarily ignoring notification {0} till {1} due to recent failures.", notification.Definition.Name, notificationStatus.DisabledTill.Value.ToLocalTime()); continue; diff --git a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs index e05dcb755..c336dafd7 100644 --- a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs +++ b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Net; - using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -112,18 +111,18 @@ namespace NzbDrone.Core.Notifications.Ntfy { try { - requestBuilder.Headers.Add("X-Title", title); - requestBuilder.Headers.Add("X-Message", message); - requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString()); + requestBuilder.AddQueryParam("title", title); + requestBuilder.AddQueryParam("message", message); + requestBuilder.AddQueryParam("priority", settings.Priority.ToString()); if (settings.Tags.Any()) { - requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(",")); + requestBuilder.AddQueryParam("tags", settings.Tags.Join(",")); } if (!settings.ClickUrl.IsNullOrWhiteSpace()) { - requestBuilder.Headers.Add("X-Click", settings.ClickUrl); + requestBuilder.AddQueryParam("click", settings.ClickUrl); } if (!settings.AccessToken.IsNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 6cf9b97e0..916aa6312 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -18,22 +18,30 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnGrab(GrabMessage message) { - _proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? RELEASE_GRABBED_TITLE_BRANDED : RELEASE_GRABBED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { - _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; + + _proxy.SendNotification(title, healthCheck.Message, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; + + _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { - _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; + + _proxy.SendNotification(title, updateMessage.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs index ec17ad4aa..ca235da3b 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs @@ -49,10 +49,11 @@ namespace NzbDrone.Core.Notifications.Telegram { try { + const string brandedTitle = "Prowlarr - Test Notification"; const string title = "Test Notification"; const string body = "This is a test message from Prowlarr"; - SendNotification(title, body, settings); + SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index 774fd4ca0..4ff40ae93 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -32,6 +32,9 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(3, Label = "Send Silently", Type = FieldType.Checkbox, HelpText = "Sends the message silently. Users will receive a notification with no sound")] public bool SendSilently { get; set; } + [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] + public bool IncludeAppNameInTitle { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs index 295d62986..ad98e5440 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookRelease.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Notifications.Webhook @@ -13,10 +16,18 @@ namespace NzbDrone.Core.Notifications.Webhook ReleaseTitle = release.Title; Indexer = release.Indexer; Size = release.Size; + Categories = release.Categories.Select(f => f.Name).ToList(); + Genres = release.Genres.ToList(); + IndexerFlags = release.IndexerFlags.Select(f => f.Name).ToHashSet(); + PublishDate = release.PublishDate; } public string ReleaseTitle { get; set; } public string Indexer { get; set; } public long? Size { get; set; } + public List<string> Categories { get; set; } + public List<string> Genres { get; set; } + public HashSet<string> IndexerFlags { get; set; } + public DateTime? PublishDate { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 7d8c83d50..6f0788607 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Parser.Model { public ReleaseInfo() { + Genres = new List<string>(); IndexerFlags = new HashSet<IndexerFlag>(); Categories = new List<IndexerCategory>(); Languages = new List<string>(); diff --git a/src/NzbDrone.Core/Parser/ParseUtil.cs b/src/NzbDrone.Core/Parser/ParseUtil.cs index ea80afb20..3927adff9 100644 --- a/src/NzbDrone.Core/Parser/ParseUtil.cs +++ b/src/NzbDrone.Core/Parser/ParseUtil.cs @@ -15,6 +15,11 @@ namespace NzbDrone.Core.Parser private static string NormalizeNumber(string s, bool isInt = false) { + if (s == null) + { + return null; + } + var valStr = new string(s.Where(c => char.IsDigit(c) || c == '.' || c == ',').ToArray()); valStr = valStr.Trim().Replace("-", "0"); diff --git a/src/NzbDrone.Core/Profiles/AppSyncProfileService.cs b/src/NzbDrone.Core/Profiles/AppSyncProfileService.cs index 135cd9e2a..095f3abd9 100644 --- a/src/NzbDrone.Core/Profiles/AppSyncProfileService.cs +++ b/src/NzbDrone.Core/Profiles/AppSyncProfileService.cs @@ -86,9 +86,9 @@ namespace NzbDrone.Core.Profiles var qualityProfile = new AppSyncProfile { Name = name, + EnableRss = true, EnableAutomaticSearch = true, EnableInteractiveSearch = true, - EnableRss = true, MinimumSeeders = 1 }; diff --git a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs index fba9ca3f3..09fecee2c 100644 --- a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs +++ b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs @@ -1,10 +1,13 @@ -using System; +using System; +using System.Threading; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.ProgressMessaging { public static class ProgressMessageContext { + private static AsyncLocal<CommandModel> _commandModelAsync = new AsyncLocal<CommandModel>(); + [ThreadStatic] private static CommandModel _commandModel; @@ -13,8 +16,15 @@ namespace NzbDrone.Core.ProgressMessaging public static CommandModel CommandModel { - get { return _commandModel; } - set { _commandModel = value; } + get + { + return _commandModel ?? _commandModelAsync.Value; + } + set + { + _commandModel = value; + _commandModelAsync.Value = value; + } } public static bool LockReentrancy() diff --git a/src/NzbDrone.Core/Prowlarr.Core.csproj b/src/NzbDrone.Core/Prowlarr.Core.csproj index 0333bb54b..3f70dd577 100644 --- a/src/NzbDrone.Core/Prowlarr.Core.csproj +++ b/src/NzbDrone.Core/Prowlarr.Core.csproj @@ -4,25 +4,28 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="AngleSharp.Xml" Version="1.0.0" /> - <PackageReference Include="Dapper" Version="2.0.123" /> - <PackageReference Include="MailKit" Version="3.6.0" /> - <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.25" /> + <PackageReference Include="Dapper" Version="2.0.151" /> + <PackageReference Include="Diacritical.Net" Version="1.0.4" /> + <PackageReference Include="MailKit" Version="4.8.0" /> + <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.35" /> <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" /> + <PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.7" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> - <PackageReference Include="Npgsql" Version="7.0.6" /> + <PackageReference Include="Npgsql" Version="7.0.10" /> + <PackageReference Include="Polly" Version="8.5.2" /> <PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> - <PackageReference Include="System.Memory" Version="4.5.5" /> + <PackageReference Include="System.Memory" Version="4.6.3" /> <PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" /> <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="5.2.0" /> + <PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> - <PackageReference Include="System.Text.Json" Version="6.0.9" /> + <PackageReference Include="System.Text.Json" Version="6.0.11" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> - <PackageReference Include="YamlDotNet" Version="13.7.1" /> - <PackageReference Include="AngleSharp" Version="1.0.6" /> + <PackageReference Include="YamlDotNet" Version="13.1.1" /> + <PackageReference Include="AngleSharp" Version="1.2.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" /> diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index fad090940..73e82f30a 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -127,10 +127,12 @@ namespace NzbDrone.Core.ThingiProvider public virtual IEnumerable<TProviderDefinition> Update(IEnumerable<TProviderDefinition> definitions) { - _providerRepository.UpdateMany(definitions.ToList()); - _eventAggregator.PublishEvent(new ProviderBulkUpdatedEvent<TProvider>(definitions)); + var providerDefinitions = definitions.ToList(); - return definitions; + _providerRepository.UpdateMany(providerDefinitions); + _eventAggregator.PublishEvent(new ProviderBulkUpdatedEvent<TProvider>(providerDefinitions)); + + return providerDefinitions; } public void Delete(int id) diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs index 7a2d1bc78..2748d30b5 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.ThingiProvider.Status where TModel : ProviderStatusBase, new() { TModel FindByProviderId(int providerId); + void DeleteByProviderId(int providerId); } public class ProviderStatusRepository<TModel> : BasicRepository<TModel>, IProviderStatusRepository<TModel> @@ -22,5 +23,10 @@ namespace NzbDrone.Core.ThingiProvider.Status { return Query(x => x.ProviderId == providerId).SingleOrDefault(); } + + public void DeleteByProviderId(int providerId) + { + Delete(c => c.ProviderId == providerId); + } } } diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs index 6279c6e35..dc9f6e807 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs @@ -151,12 +151,7 @@ namespace NzbDrone.Core.ThingiProvider.Status public virtual void HandleAsync(ProviderDeletedEvent<TProvider> message) { - var providerStatus = _providerStatusRepository.FindByProviderId(message.ProviderId); - - if (providerStatus != null) - { - _providerStatusRepository.Delete(providerStatus); - } + _providerStatusRepository.DeleteByProviderId(message.ProviderId); } } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationCheckUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs similarity index 65% rename from src/NzbDrone.Core/Update/Commands/ApplicationCheckUpdateCommand.cs rename to src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs index 6987af3fa..fa2cfbf41 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationCheckUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs @@ -2,10 +2,12 @@ using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Update.Commands { - public class ApplicationCheckUpdateCommand : Command + public class ApplicationUpdateCheckCommand : Command { public override bool SendUpdatesToClient => true; public override string CompletionMessage => null; + + public bool InstallMajorUpdate { get; set; } } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 0ca1d8074..6980af708 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -4,9 +4,8 @@ namespace NzbDrone.Core.Update.Commands { public class ApplicationUpdateCommand : Command { + public bool InstallMajorUpdate { get; set; } public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; - - public override string CompletionMessage => null; } } diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs index 09cf70602..7be7349e1 100644 --- a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NLog; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Update.History.Events; @@ -18,13 +19,15 @@ namespace NzbDrone.Core.Update.History { private readonly IUpdateHistoryRepository _repository; private readonly IEventAggregator _eventAggregator; + private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; private Version _prevVersion; - public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, Logger logger) + public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, IConfigFileProvider configFileProvider, Logger logger) { _repository = repository; _eventAggregator = eventAggregator; + _configFileProvider = configFileProvider; _logger = logger; } @@ -58,7 +61,7 @@ namespace NzbDrone.Core.Update.History public void Handle(ApplicationStartedEvent message) { - if (BuildInfo.Version.Major == 10) + if (BuildInfo.Version.Major == 10 || !_configFileProvider.LogDbEnabled) { // Don't save dev versions, they change constantly return; diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 99744922e..793e1a8ac 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -20,7 +20,7 @@ using NzbDrone.Core.Update.Commands; namespace NzbDrone.Core.Update { - public class InstallUpdateService : IExecute<ApplicationCheckUpdateCommand>, IExecute<ApplicationUpdateCommand>, IHandle<ApplicationStartingEvent> + public class InstallUpdateService : IExecute<ApplicationUpdateCommand>, IExecute<ApplicationUpdateCheckCommand>, IHandle<ApplicationStartingEvent> { private readonly ICheckUpdateService _checkUpdateService; private readonly Logger _logger; @@ -83,7 +83,7 @@ namespace NzbDrone.Core.Update { EnsureAppDataSafety(); - if (OsInfo.IsWindows || _configFileProvider.UpdateMechanism != UpdateMechanism.Script) + if (_configFileProvider.UpdateMechanism != UpdateMechanism.Script) { var startupFolder = _appFolderInfo.StartUpFolder; var uiFolder = Path.Combine(startupFolder, "UI"); @@ -105,8 +105,14 @@ namespace NzbDrone.Core.Update return false; } + var tempFolder = _appFolderInfo.TempFolder; var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder(); + if (_diskProvider.GetTotalSize(tempFolder) < 1.Gigabytes()) + { + _logger.Warn("Temporary location '{0}' has less than 1 GB free space, Prowlarr may not be able to update itself.", tempFolder); + } + var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); if (_diskProvider.FolderExists(updateSandboxFolder)) @@ -137,7 +143,7 @@ namespace NzbDrone.Core.Update _backupService.Backup(BackupType.Update); - if (OsInfo.IsNotWindows && _configFileProvider.UpdateMechanism == UpdateMechanism.Script) + if (_configFileProvider.UpdateMechanism == UpdateMechanism.Script) { InstallUpdateWithScript(updateSandboxFolder); return true; @@ -225,7 +231,7 @@ namespace NzbDrone.Core.Update } } - private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger) + private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger, bool installMajorUpdate) { _logger.ProgressDebug("Checking for updates"); @@ -237,18 +243,24 @@ namespace NzbDrone.Core.Update return null; } - if (_osInfo.IsDocker) + if (latestAvailable.Version.Major > BuildInfo.Version.Major && !installMajorUpdate) { - _logger.ProgressDebug("Updating is disabled inside a docker container. Please update the container image."); + _logger.ProgressInfo("Unable to install major update, please update update manually from System: Updates"); return null; } - if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) + if (!_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) { _logger.ProgressDebug("Auto-update not enabled, not installing available update."); return null; } + if (_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && _deploymentInfoProvider.PackageUpdateMechanism == UpdateMechanism.Docker) + { + _logger.ProgressDebug("Built-In updater disabled inside a docker container. Please update the container image."); + return null; + } + // Safety net, ConfigureUpdateMechanism should take care of invalid settings if (_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && _deploymentInfoProvider.IsExternalUpdateMechanism) { @@ -264,9 +276,9 @@ namespace NzbDrone.Core.Update return latestAvailable; } - public void Execute(ApplicationCheckUpdateCommand message) + public void Execute(ApplicationUpdateCheckCommand message) { - if (GetUpdatePackage(message.Trigger) != null) + if (GetUpdatePackage(message.Trigger, true) != null) { _commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger); } @@ -274,7 +286,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCommand message) { - var latestAvailable = GetUpdatePackage(message.Trigger); + var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate); if (latestAvailable != null) { diff --git a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs index 4796a68e2..a038311e2 100644 --- a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs +++ b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Update { var branch = _configFileProvider.Branch; var version = BuildInfo.Version; - var prevVersion = _updateHistoryService.PreviouslyInstalled(); + var prevVersion = _configFileProvider.LogDbEnabled ? _updateHistoryService.PreviouslyInstalled() : null; return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion); } } diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index 17743c8b0..58d9872cc 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -47,6 +47,7 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtime", "netcore") .AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("dbType", _mainDatabase.DatabaseType) + .AddQueryParam("includeMajorVersion", true) .SetSegment("branch", branch); if (_analyticsService.IsEnabled) diff --git a/src/NzbDrone.Core/Validation/IpValidation.cs b/src/NzbDrone.Core/Validation/IpValidation.cs index eb5863caa..f4afa1f66 100644 --- a/src/NzbDrone.Core/Validation/IpValidation.cs +++ b/src/NzbDrone.Core/Validation/IpValidation.cs @@ -1,5 +1,4 @@ using FluentValidation; -using FluentValidation.Validators; using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Validation @@ -10,10 +9,5 @@ namespace NzbDrone.Core.Validation { return ruleBuilder.Must(x => x.IsValidIpAddress()).WithMessage("Must contain wildcard (*) or a valid IP Address"); } - - public static IRuleBuilderOptions<T, string> NotListenAllIp4Address<T>(this IRuleBuilder<T, string> ruleBuilder) - { - return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!0\.0\.0\.0)")).WithMessage("Use * instead of 0.0.0.0"); - } } } diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 874c3e576..c04a0e705 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Common; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; @@ -46,6 +47,11 @@ namespace NzbDrone.App.Test container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object); container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object); container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object); + container.RegisterInstance<IOptions<AuthOptions>>(new Mock<IOptions<AuthOptions>>().Object); + container.RegisterInstance<IOptions<AppOptions>>(new Mock<IOptions<AppOptions>>().Object); + container.RegisterInstance<IOptions<ServerOptions>>(new Mock<IOptions<ServerOptions>>().Object); + container.RegisterInstance<IOptions<UpdateOptions>>(new Mock<IOptions<UpdateOptions>>().Object); + container.RegisterInstance<IOptions<LogOptions>>(new Mock<IOptions<LogOptions>>().Object); _container = container.GetServiceProvider(); } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 5f4a750e2..3d4e82964 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -22,6 +22,7 @@ using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Common.Options; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions; @@ -93,10 +94,24 @@ namespace NzbDrone.Host .AddStartupContext(startupContext) .Resolve<UtilityModeRouter>() .Route(appMode); + + if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true)) + { + c.AddLogDatabase(); + } + else + { + c.AddDummyLogDatabase(); + } }) .ConfigureServices(services => { services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres")); + services.Configure<AppOptions>(config.GetSection("Prowlarr:App")); + services.Configure<AuthOptions>(config.GetSection("Prowlarr:Auth")); + services.Configure<ServerOptions>(config.GetSection("Prowlarr:Server")); + services.Configure<LogOptions>(config.GetSection("Prowlarr:Log")); + services.Configure<UpdateOptions>(config.GetSection("Prowlarr:Update")); }).Build(); break; @@ -124,12 +139,13 @@ namespace NzbDrone.Host { var config = GetConfiguration(context); - var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*"); - var port = config.GetValue(nameof(ConfigFileProvider.Port), ConfigFileProvider.DEFAULT_PORT); - var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), ConfigFileProvider.DEFAULT_SSL_PORT); - var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); - var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath)); - var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword)); + var bindAddress = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.BindAddress)}") ?? config.GetValue(nameof(ConfigFileProvider.BindAddress), "*"); + var port = config.GetValue<int?>($"Prowlarr:Server:{nameof(ServerOptions.Port)}") ?? config.GetValue(nameof(ConfigFileProvider.Port), ConfigFileProvider.DEFAULT_PORT); + var sslPort = config.GetValue<int?>($"Prowlarr:Server:{nameof(ServerOptions.SslPort)}") ?? config.GetValue(nameof(ConfigFileProvider.SslPort), ConfigFileProvider.DEFAULT_SSL_PORT); + var enableSsl = config.GetValue<bool?>($"Prowlarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); + var sslCertPath = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath)); + var sslCertPassword = config.GetValue<string>($"Prowlarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword)); + var logDbEnabled = config.GetValue<bool?>($"Prowlarr:Log:{nameof(LogOptions.DbEnabled)}") ?? config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true); var urls = new List<string> { BuildUrl("http", bindAddress, port) }; @@ -147,10 +163,24 @@ namespace NzbDrone.Host .AddNzbDroneLogger() .AddDatabase() .AddStartupContext(context); + + if (logDbEnabled) + { + c.AddLogDatabase(); + } + else + { + c.AddDummyLogDatabase(); + } }) .ConfigureServices(services => { services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres")); + services.Configure<AppOptions>(config.GetSection("Prowlarr:App")); + services.Configure<AuthOptions>(config.GetSection("Prowlarr:Auth")); + services.Configure<ServerOptions>(config.GetSection("Prowlarr:Server")); + services.Configure<LogOptions>(config.GetSection("Prowlarr:Log")); + services.Configure<UpdateOptions>(config.GetSection("Prowlarr:Update")); services.Configure<FormOptions>(x => { //Double the default multipart body length from 128 MB to 256 MB diff --git a/src/NzbDrone.Host/Prowlarr.Host.csproj b/src/NzbDrone.Host/Prowlarr.Host.csproj index 4eb549ced..12bd5168e 100644 --- a/src/NzbDrone.Host/Prowlarr.Host.csproj +++ b/src/NzbDrone.Host/Prowlarr.Host.csproj @@ -4,10 +4,10 @@ <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> - <PackageReference Include="NLog.Extensions.Logging" Version="5.3.0" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> - <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.4.0" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.3" /> + <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" /> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> </ItemGroup> diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index f0d96c04f..5a1dc92d9 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -26,6 +27,7 @@ using NzbDrone.SignalR; using Prowlarr.Api.V1.System; using Prowlarr.Http; using Prowlarr.Http.Authentication; +using Prowlarr.Http.ClientSchema; using Prowlarr.Http.ErrorManagement; using Prowlarr.Http.Frontend; using Prowlarr.Http.Middleware; @@ -56,7 +58,7 @@ namespace NzbDrone.Host services.Configure<ForwardedHeadersOptions>(options => { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); }); @@ -133,7 +135,7 @@ namespace NzbDrone.Host Name = "apikey", Type = SecuritySchemeType.ApiKey, Scheme = "apiKey", - Description = "Apikey passed as header", + Description = "Apikey passed as query parameter", In = ParameterLocation.Query, Reference = new OpenApiReference { @@ -208,6 +210,7 @@ namespace NzbDrone.Host } public void Configure(IApplicationBuilder app, + IContainer container, IStartupContext startupContext, Lazy<IMainDatabase> mainDatabaseFactory, Lazy<ILogDatabase> logDatabaseFactory, @@ -235,9 +238,14 @@ namespace NzbDrone.Host // instantiate the databases to initialize/migrate them _ = mainDatabaseFactory.Value; - _ = logDatabaseFactory.Value; - dbTarget.Register(); + if (configFileProvider.LogDbEnabled) + { + _ = logDatabaseFactory.Value; + dbTarget.Register(); + } + + SchemaBuilder.Initialize(container); if (OsInfo.IsNotWindows) { diff --git a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs index 91d0962ab..f8a8ccd51 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Test] public void should_be_able_to_run_update_check() { - var response = Commands.Post(new SimpleCommandResource { Name = "applicationcheckupdate" }); + var response = Commands.Post(new SimpleCommandResource { Name = "applicationupdatecheck" }); response.Id.Should().NotBe(0); } diff --git a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs index ba88aca50..f8213f186 100644 --- a/src/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/src/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -1,4 +1,5 @@ -using Prowlarr.Api.V1.Indexers; +using System.Collections.Generic; +using Prowlarr.Api.V1.Indexers; using RestSharp; namespace NzbDrone.Integration.Test.Client @@ -9,5 +10,11 @@ namespace NzbDrone.Integration.Test.Client : base(restClient, apiKey) { } + + public List<IndexerResource> Schema() + { + var request = BuildRequest("/schema"); + return Get<List<IndexerResource>>(request); + } } } diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index c58d35f89..cb060c56f 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,13 +1,14 @@ +using System; +using System.Linq; using System.Threading; using NLog; using NUnit.Framework; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Migration.Framework; -using NzbDrone.Core.Indexers.Definitions.FileList; +using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; using NzbDrone.Test.Common.Datastore; -using Prowlarr.Http.ClientSchema; namespace NzbDrone.Integration.Test { @@ -49,16 +50,19 @@ namespace NzbDrone.Integration.Test { WaitForCompletion(() => Tasks.All().SelectList(x => x.TaskName).Contains("CheckHealth"), 20000); - Indexers.Post(new Prowlarr.Api.V1.Indexers.IndexerResource + var indexer = Indexers.Schema().FirstOrDefault(i => i.Implementation == nameof(Newznab)); + + if (indexer == null) { - Enable = false, - ConfigContract = nameof(FileListSettings), - Implementation = nameof(FileList), - Name = "NewznabTest", - Protocol = Core.Indexers.DownloadProtocol.Usenet, - AppProfileId = 1, - Fields = SchemaBuilder.ToSchema(new FileListSettings()) - }); + throw new NullReferenceException("Expected valid indexer schema, found null"); + } + + indexer.Enable = false; + indexer.ConfigContract = nameof(NewznabSettings); + indexer.Implementation = nameof(Newznab); + indexer.Name = "NewznabTest"; + indexer.Protocol = Core.Indexers.DownloadProtocol.Usenet; + indexer.AppProfileId = 1; // Change Console Log Level to Debug so we get more details. var config = HostConfig.Get(1); diff --git a/src/NzbDrone.Integration.Test/Prowlarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Prowlarr.Integration.Test.csproj index ef7cc21f3..a4c142248 100644 --- a/src/NzbDrone.Integration.Test/Prowlarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Prowlarr.Integration.Test.csproj @@ -4,7 +4,7 @@ <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.35" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> diff --git a/src/NzbDrone.Mono/Disk/FindDriveType.cs b/src/NzbDrone.Mono/Disk/FindDriveType.cs index d0481c3d4..08cc611de 100644 --- a/src/NzbDrone.Mono/Disk/FindDriveType.cs +++ b/src/NzbDrone.Mono/Disk/FindDriveType.cs @@ -6,15 +6,16 @@ namespace NzbDrone.Mono.Disk { public static class FindDriveType { - private static readonly Dictionary<string, DriveType> DriveTypeMap = new Dictionary<string, DriveType> - { - { "afpfs", DriveType.Network }, - { "apfs", DriveType.Fixed }, - { "fuse.mergerfs", DriveType.Fixed }, - { "fuse.glusterfs", DriveType.Network }, - { "nullfs", DriveType.Fixed }, - { "zfs", DriveType.Fixed } - }; + private static readonly Dictionary<string, DriveType> DriveTypeMap = new () + { + { "afpfs", DriveType.Network }, + { "apfs", DriveType.Fixed }, + { "fuse.mergerfs", DriveType.Fixed }, + { "fuse.shfs", DriveType.Fixed }, + { "fuse.glusterfs", DriveType.Network }, + { "nullfs", DriveType.Fixed }, + { "zfs", DriveType.Fixed } + }; public static DriveType Find(string driveFormat) { diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 945dd3aae..821b91fa7 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -23,6 +23,8 @@ namespace NzbDrone.Test.Common.AutoMoq SetupAutoMoqer(CreateTestContainer(new Container())); } + public IContainer Container => _container; + public virtual T Resolve<T>() { var result = _container.Resolve<T>(); diff --git a/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj b/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj index 2b6c7e54b..b9ccacd9e 100644 --- a/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/Prowlarr.Test.Common.csproj @@ -6,8 +6,8 @@ <PackageReference Include="FluentAssertions" Version="6.11.0" /> <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Moq" Version="4.17.2" /> - <PackageReference Include="NLog" Version="5.2.0" /> - <PackageReference Include="NUnit" Version="3.13.3" /> + <PackageReference Include="NLog" Version="5.4.0" /> + <PackageReference Include="NUnit" Version="3.14.0" /> <PackageReference Include="RestSharp" Version="106.15.0" /> <PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" /> </ItemGroup> diff --git a/src/NzbDrone.Update/Prowlarr.Update.csproj b/src/NzbDrone.Update/Prowlarr.Update.csproj index 2d2812f00..09b95e2f8 100644 --- a/src/NzbDrone.Update/Prowlarr.Update.csproj +++ b/src/NzbDrone.Update/Prowlarr.Update.csproj @@ -6,7 +6,7 @@ <ItemGroup> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> - <PackageReference Include="NLog" Version="5.2.0" /> + <PackageReference Include="NLog" Version="5.4.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" /> diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index 1b2f4d90d..5ea53453b 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -122,9 +122,6 @@ namespace NzbDrone.Update.UpdateEngine try { - _logger.Info("Emptying installation folder"); - _diskProvider.EmptyFolder(installationFolder); - _logger.Info("Copying new files to target folder"); _diskTransferService.MirrorFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder); diff --git a/src/NzbDrone.Windows/Disk/DiskProvider.cs b/src/NzbDrone.Windows/Disk/DiskProvider.cs index ebdf026e1..f0661e245 100644 --- a/src/NzbDrone.Windows/Disk/DiskProvider.cs +++ b/src/NzbDrone.Windows/Disk/DiskProvider.cs @@ -170,6 +170,11 @@ namespace NzbDrone.Windows.Disk { try { + if (source.Length > 256 && !source.StartsWith(@"\\?\")) + { + source = @"\\?\" + source; + } + return CreateHardLink(destination, source, IntPtr.Zero); } catch (Exception ex) diff --git a/src/NzbDrone.Windows/Prowlarr.Windows.csproj b/src/NzbDrone.Windows/Prowlarr.Windows.csproj index e4407e1cd..4a9f864e5 100644 --- a/src/NzbDrone.Windows/Prowlarr.Windows.csproj +++ b/src/NzbDrone.Windows/Prowlarr.Windows.csproj @@ -4,7 +4,7 @@ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> - <PackageReference Include="NLog" Version="5.2.0" /> + <PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> </ItemGroup> <ItemGroup> diff --git a/src/Prowlarr.Api.V1.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/Prowlarr.Api.V1.Test/ClientSchemaTests/SchemaBuilderFixture.cs index 16b54921f..2b982e78a 100644 --- a/src/Prowlarr.Api.V1.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/Prowlarr.Api.V1.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Localization; using NzbDrone.Test.Common; using Prowlarr.Http.ClientSchema; @@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests [TestFixture] public class SchemaBuilderFixture : TestBase { + [SetUp] + public void Setup() + { + Mocker.GetMock<ILocalizationService>() + .Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>())) + .Returns<string, Dictionary<string, object>>((s, d) => s); + + SchemaBuilder.Initialize(Mocker.Container); + } + [Test] public void should_return_field_for_every_property() { diff --git a/src/Prowlarr.Api.V1/Applications/ApplicationController.cs b/src/Prowlarr.Api.V1/Applications/ApplicationController.cs index c3819d8f9..a63beedcf 100644 --- a/src/Prowlarr.Api.V1/Applications/ApplicationController.cs +++ b/src/Prowlarr.Api.V1/Applications/ApplicationController.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Applications; +using NzbDrone.SignalR; using Prowlarr.Http; namespace Prowlarr.Api.V1.Applications @@ -9,8 +10,8 @@ namespace Prowlarr.Api.V1.Applications public static readonly ApplicationResourceMapper ResourceMapper = new (); public static readonly ApplicationBulkResourceMapper BulkResourceMapper = new (); - public ApplicationController(ApplicationFactory applicationsFactory) - : base(applicationsFactory, "applications", ResourceMapper, BulkResourceMapper) + public ApplicationController(IBroadcastSignalRMessage signalRBroadcaster, ApplicationFactory applicationsFactory) + : base(signalRBroadcaster, applicationsFactory, "applications", ResourceMapper, BulkResourceMapper) { } } diff --git a/src/Prowlarr.Api.V1/Applications/ApplicationResource.cs b/src/Prowlarr.Api.V1/Applications/ApplicationResource.cs index 5c1374ea6..417ba238a 100644 --- a/src/Prowlarr.Api.V1/Applications/ApplicationResource.cs +++ b/src/Prowlarr.Api.V1/Applications/ApplicationResource.cs @@ -1,10 +1,15 @@ using NzbDrone.Core.Applications; +using Swashbuckle.AspNetCore.Annotations; namespace Prowlarr.Api.V1.Applications { public class ApplicationResource : ProviderResource<ApplicationResource> { public ApplicationSyncLevel SyncLevel { get; set; } + + [SwaggerIgnore] + public bool Enable { get; set; } + public string TestCommand { get; set; } } @@ -20,18 +25,19 @@ namespace Prowlarr.Api.V1.Applications var resource = base.ToResource(definition); resource.SyncLevel = definition.SyncLevel; + resource.Enable = definition.Enable; return resource; } - public override ApplicationDefinition ToModel(ApplicationResource resource) + public override ApplicationDefinition ToModel(ApplicationResource resource, ApplicationDefinition existingDefinition) { if (resource == null) { return default; } - var definition = base.ToModel(resource); + var definition = base.ToModel(resource, existingDefinition); definition.SyncLevel = resource.SyncLevel; diff --git a/src/Prowlarr.Api.V1/Commands/CommandController.cs b/src/Prowlarr.Api.V1/Commands/CommandController.cs index cbca618a3..714685b22 100644 --- a/src/Prowlarr.Api.V1/Commands/CommandController.cs +++ b/src/Prowlarr.Api.V1/Commands/CommandController.cs @@ -50,7 +50,7 @@ namespace Prowlarr.Api.V1.Commands [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<CommandResource> StartCommand(CommandResource commandResource) + public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource) { var commandType = _knownTypes.GetImplementations(typeof(Command)) @@ -61,9 +61,8 @@ namespace Prowlarr.Api.V1.Commands using var reader = new StreamReader(Request.Body); var body = reader.ReadToEnd(); - dynamic command = STJson.Deserialize(body, commandType); + var command = STJson.Deserialize(body, commandType) as Command; - command.Trigger = CommandTrigger.Manual; command.SuppressMessages = !command.SendUpdatesToClient; command.SendUpdatesToClient = true; command.ClientUserAgent = Request.Headers["User-Agent"]; diff --git a/src/Prowlarr.Api.V1/Config/ConfigController.cs b/src/Prowlarr.Api.V1/Config/ConfigController.cs index 120a69352..83e3cd666 100644 --- a/src/Prowlarr.Api.V1/Config/ConfigController.cs +++ b/src/Prowlarr.Api.V1/Config/ConfigController.cs @@ -35,7 +35,7 @@ namespace Prowlarr.Api.V1.Config [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public virtual ActionResult<TResource> SaveConfig(TResource resource) + public virtual ActionResult<TResource> SaveConfig([FromBody] TResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Prowlarr.Api.V1/Config/HostConfigController.cs b/src/Prowlarr.Api.V1/Config/HostConfigController.cs index 427fc8db3..a844f8d2e 100644 --- a/src/Prowlarr.Api.V1/Config/HostConfigController.cs +++ b/src/Prowlarr.Api.V1/Config/HostConfigController.cs @@ -34,7 +34,6 @@ namespace Prowlarr.Api.V1.Config SharedValidator.RuleFor(c => c.BindAddress) .ValidIpAddress() - .NotListenAllIp4Address() .When(c => c.BindAddress != "*" && c.BindAddress != "localhost"); SharedValidator.RuleFor(c => c.Port).ValidPort(); @@ -61,6 +60,8 @@ namespace Prowlarr.Api.V1.Config .Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password") .When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.LogSizeLimit).InclusiveBetween(1, 10); + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); @@ -125,7 +126,7 @@ namespace Prowlarr.Api.V1.Config [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource) + public ActionResult<HostConfigResource> SaveHostConfig([FromBody] HostConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs index 425453913..4bd0cd10a 100644 --- a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs +++ b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs @@ -21,6 +21,7 @@ namespace Prowlarr.Api.V1.Config public string Password { get; set; } public string PasswordConfirmation { get; set; } public string LogLevel { get; set; } + public int LogSizeLimit { get; set; } public string ConsoleLogLevel { get; set; } public string Branch { get; set; } public string ApiKey { get; set; } @@ -45,6 +46,7 @@ namespace Prowlarr.Api.V1.Config public int BackupInterval { get; set; } public int BackupRetention { get; set; } public int HistoryCleanupDays { get; set; } + public bool TrustCgnatIpAddresses { get; set; } } public static class HostConfigResourceMapper @@ -66,6 +68,7 @@ namespace Prowlarr.Api.V1.Config //Username //Password LogLevel = model.LogLevel, + LogSizeLimit = model.LogSizeLimit, ConsoleLogLevel = model.ConsoleLogLevel, Branch = model.Branch, ApiKey = model.ApiKey, diff --git a/src/Prowlarr.Api.V1/Config/UiConfigController.cs b/src/Prowlarr.Api.V1/Config/UiConfigController.cs index a22852863..b96b07444 100644 --- a/src/Prowlarr.Api.V1/Config/UiConfigController.cs +++ b/src/Prowlarr.Api.V1/Config/UiConfigController.cs @@ -35,7 +35,7 @@ namespace Prowlarr.Api.V1.Config [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource) + public override ActionResult<UiConfigResource> SaveConfig([FromBody] UiConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) diff --git a/src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs b/src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs index b4a578c09..6a924c6a8 100644 --- a/src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs +++ b/src/Prowlarr.Api.V1/CustomFilters/CustomFilterController.cs @@ -32,7 +32,7 @@ namespace Prowlarr.Api.V1.CustomFilters [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource) + public ActionResult<CustomFilterResource> AddCustomFilter([FromBody] CustomFilterResource resource) { var customFilter = _customFilterService.Add(resource.ToModel()); @@ -42,7 +42,7 @@ namespace Prowlarr.Api.V1.CustomFilters [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource) + public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFilterResource resource) { _customFilterService.Update(resource.ToModel()); return Accepted(resource.Id); diff --git a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientController.cs b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientController.cs index 5dd43ea7d..347c7e9c6 100644 --- a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientController.cs +++ b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientController.cs @@ -1,4 +1,6 @@ +using FluentValidation; using NzbDrone.Core.Download; +using NzbDrone.SignalR; using Prowlarr.Http; namespace Prowlarr.Api.V1.DownloadClient @@ -9,9 +11,10 @@ namespace Prowlarr.Api.V1.DownloadClient public static readonly DownloadClientResourceMapper ResourceMapper = new (); public static readonly DownloadClientBulkResourceMapper BulkResourceMapper = new (); - public DownloadClientController(IDownloadClientFactory downloadClientFactory) - : base(downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper) + public DownloadClientController(IBroadcastSignalRMessage signalRBroadcaster, IDownloadClientFactory downloadClientFactory) + : base(signalRBroadcaster, downloadClientFactory, "downloadclient", ResourceMapper, BulkResourceMapper) { + SharedValidator.RuleFor(c => c.Priority).InclusiveBetween(1, 50); } } } diff --git a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs index 7376d6fe4..4ff1e5b53 100644 --- a/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs +++ b/src/Prowlarr.Api.V1/DownloadClient/DownloadClientResource.cs @@ -33,14 +33,14 @@ namespace Prowlarr.Api.V1.DownloadClient return resource; } - public override DownloadClientDefinition ToModel(DownloadClientResource resource) + public override DownloadClientDefinition ToModel(DownloadClientResource resource, DownloadClientDefinition existingDefinition) { if (resource == null) { return null; } - var definition = base.ToModel(resource); + var definition = base.ToModel(resource, existingDefinition); definition.Enable = resource.Enable; definition.Protocol = resource.Protocol; diff --git a/src/Prowlarr.Api.V1/Health/HealthResource.cs b/src/Prowlarr.Api.V1/Health/HealthResource.cs index 1e2c8ffa5..3f357285d 100644 --- a/src/Prowlarr.Api.V1/Health/HealthResource.cs +++ b/src/Prowlarr.Api.V1/Health/HealthResource.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.Http; using NzbDrone.Core.HealthCheck; using Prowlarr.Http.REST; @@ -11,7 +10,7 @@ namespace Prowlarr.Api.V1.Health public string Source { get; set; } public HealthCheckResult Type { get; set; } public string Message { get; set; } - public HttpUri WikiUrl { get; set; } + public string WikiUrl { get; set; } } public static class HealthResourceMapper @@ -29,7 +28,7 @@ namespace Prowlarr.Api.V1.Health Source = model.Source.Name, Type = model.Type, Message = model.Message, - WikiUrl = model.WikiUrl + WikiUrl = model.WikiUrl.FullUri }; } diff --git a/src/Prowlarr.Api.V1/History/HistoryController.cs b/src/Prowlarr.Api.V1/History/HistoryController.cs index 73508a47b..a744ffc2f 100644 --- a/src/Prowlarr.Api.V1/History/HistoryController.cs +++ b/src/Prowlarr.Api.V1/History/HistoryController.cs @@ -22,15 +22,20 @@ namespace Prowlarr.Api.V1.History [HttpGet] [Produces("application/json")] - public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, int? eventType, bool? successful, string downloadId) + public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[] eventTypes, bool? successful, string downloadId, [FromQuery] int[] indexerIds = null) { var pagingResource = new PagingResource<HistoryResource>(paging); - var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending); + var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>( + new HashSet<string>(StringComparer.OrdinalIgnoreCase) + { + "date" + }, + "date", + SortDirection.Descending); - if (eventType.HasValue) + if (eventTypes != null && eventTypes.Any()) { - var filterValue = (HistoryEventType)eventType.Value; - pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); + pagingSpec.FilterExpressions.Add(v => eventTypes.Contains((int)v.EventType)); } if (successful.HasValue) @@ -44,7 +49,12 @@ namespace Prowlarr.Api.V1.History pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); } - return pagingSpec.ApplyToPage(_historyService.Paged, MapToResource); + if (indexerIds != null && indexerIds.Any()) + { + pagingSpec.FilterExpressions.Add(h => indexerIds.Contains(h.IndexerId)); + } + + return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec), MapToResource); } [HttpGet("since")] diff --git a/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyController.cs b/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyController.cs index ba6cbfbe7..d1be85292 100644 --- a/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyController.cs +++ b/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyController.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.IndexerProxies; +using NzbDrone.SignalR; using Prowlarr.Http; namespace Prowlarr.Api.V1.IndexerProxies @@ -11,8 +12,8 @@ namespace Prowlarr.Api.V1.IndexerProxies public static readonly IndexerProxyResourceMapper ResourceMapper = new (); public static readonly IndexerProxyBulkResourceMapper BulkResourceMapper = new (); - public IndexerProxyController(IndexerProxyFactory notificationFactory) - : base(notificationFactory, "indexerProxy", ResourceMapper, BulkResourceMapper) + public IndexerProxyController(IBroadcastSignalRMessage signalRBroadcaster, IndexerProxyFactory notificationFactory) + : base(signalRBroadcaster, notificationFactory, "indexerProxy", ResourceMapper, BulkResourceMapper) { } diff --git a/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyResource.cs b/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyResource.cs index 6b8719289..8416464de 100644 --- a/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyResource.cs +++ b/src/Prowlarr.Api.V1/IndexerProxies/IndexerProxyResource.cs @@ -25,14 +25,14 @@ namespace Prowlarr.Api.V1.IndexerProxies return resource; } - public override IndexerProxyDefinition ToModel(IndexerProxyResource resource) + public override IndexerProxyDefinition ToModel(IndexerProxyResource resource, IndexerProxyDefinition existingDefinition) { if (resource == null) { return default(IndexerProxyDefinition); } - var definition = base.ToModel(resource); + var definition = base.ToModel(resource, existingDefinition); return definition; } diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerBulkResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerBulkResource.cs index 7f3d281f0..7434622f2 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerBulkResource.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerBulkResource.cs @@ -12,6 +12,7 @@ namespace Prowlarr.Api.V1.Indexers public double? SeedRatio { get; set; } public int? SeedTime { get; set; } public int? PackSeedTime { get; set; } + public bool? PreferMagnetUrl { get; set; } } public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition> @@ -35,6 +36,7 @@ namespace Prowlarr.Api.V1.Indexers ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio = resource.SeedRatio ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedRatio; ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime = resource.SeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.SeedTime; ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime = resource.PackSeedTime ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PackSeedTime; + ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl = resource.PreferMagnetUrl ?? ((ITorrentIndexerSettings)existing.Settings).TorrentBaseSettings.PreferMagnetUrl; } }); diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerController.cs b/src/Prowlarr.Api.V1/Indexers/IndexerController.cs index 972ae4426..07955299d 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerController.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerController.cs @@ -1,5 +1,7 @@ +using FluentValidation; using NzbDrone.Core.Indexers; using NzbDrone.Core.Validation; +using NzbDrone.SignalR; using Prowlarr.Http; namespace Prowlarr.Api.V1.Indexers @@ -7,16 +9,19 @@ namespace Prowlarr.Api.V1.Indexers [V1ApiController] public class IndexerController : ProviderControllerBase<IndexerResource, IndexerBulkResource, IIndexer, IndexerDefinition> { - public IndexerController(IndexerFactory indexerFactory, + public IndexerController(IBroadcastSignalRMessage signalRBroadcaster, + IndexerFactory indexerFactory, IndexerResourceMapper resourceMapper, IndexerBulkResourceMapper bulkResourceMapper, AppProfileExistsValidator appProfileExistsValidator, DownloadClientExistsValidator downloadClientExistsValidator) - : base(indexerFactory, "indexer", resourceMapper, bulkResourceMapper) + : base(signalRBroadcaster, indexerFactory, "indexer", resourceMapper, bulkResourceMapper) { - Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.AppProfileId)); + SharedValidator.RuleFor(c => c.AppProfileId).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(appProfileExistsValidator); - SharedValidator.RuleFor(c => c.AppProfileId).SetValidator(appProfileExistsValidator); + SharedValidator.RuleFor(c => c.Priority).InclusiveBetween(1, 50); SharedValidator.RuleFor(c => c.DownloadClientId).SetValidator(downloadClientExistsValidator); } } diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs index 832d7f219..31624dbf2 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs @@ -60,7 +60,7 @@ namespace Prowlarr.Api.V1.Indexers if (definition.Implementation == nameof(Cardigann)) { - var extraFields = definition.ExtraFields?.Select(MapField).ToList() ?? new List<Field>(); + var extraFields = definition.ExtraFields?.Select((field, i) => MapCardigannField(definition, field, i)).ToList() ?? new List<Field>(); resource.Fields.AddRange(extraFields); @@ -104,14 +104,14 @@ namespace Prowlarr.Api.V1.Indexers return resource; } - public override IndexerDefinition ToModel(IndexerResource resource) + public override IndexerDefinition ToModel(IndexerResource resource, IndexerDefinition existingDefinition) { if (resource == null) { return null; } - var definition = base.ToModel(resource); + var definition = base.ToModel(resource, existingDefinition); if (resource.Implementation == nameof(Cardigann)) { @@ -119,20 +119,29 @@ namespace Prowlarr.Api.V1.Indexers var settings = (CardigannSettings)definition.Settings; - var cardigannDefinition = _definitionService.GetCachedDefinition(settings.DefinitionFile); - - foreach (var field in resource.Fields) + if (settings.DefinitionFile.IsNotNullOrWhiteSpace()) { - if (!standardFields.Contains(field.Name)) + var cardigannDefinition = _definitionService.GetCachedDefinition(settings.DefinitionFile); + + foreach (var field in resource.Fields) { - if (field.Name == "cardigannCaptcha") + if (!standardFields.Contains(field.Name)) { - settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty; - } - else - { - var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name); - settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value); + if (field.Name == "cardigannCaptcha") + { + settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty; + } + else + { + var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name); + + if (cardigannSetting == null) + { + throw new ArgumentOutOfRangeException(field.Name, "Unknown Cardigann setting."); + } + + settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value); + } } } } @@ -160,7 +169,7 @@ namespace Prowlarr.Api.V1.Indexers }; } - private Field MapField(SettingsField setting, int order) + private Field MapCardigannField(IndexerDefinition definition, SettingsField setting, int order) { var field = new Field { @@ -185,6 +194,34 @@ namespace Prowlarr.Api.V1.Indexers { field.Value = bool.TryParse(setting.Default, out var value) && value; } + else if (setting.Type is "info_cookie" or "info_flaresolverr" or "info_useragent" or "info_category_8000") + { + field.Type = "info"; + + switch (setting.Type) + { + case "info_cookie": + field.Label = "How to get the Cookie"; + field.Value = "<ol><li>Login to this tracker with your browser</li><li>If present in the login page, ensure you have the <b>Remember me</b> ticked and the <b>Log Me Out if IP Changes</b> unticked when you login</li><li>Navigate to the web site's torrent search page to view the list of available torrents for download</li><li>Open the <b>DevTools</b> panel by pressing <b>F12</b></li><li>Select the <b>Network</b> tab</li><li>Click on the <b>Doc</b> button (Chrome Browser) or <b>HTML</b> button (FireFox)</li><li>Refresh the page by pressing <b>F5</b></li><li>Click on the first row entry</li><li>Select the <b>Headers</b> tab on the Right panel</li><li>Find <b>'cookie:'</b> in the <b>Request Headers</b> section</li><li><b>Select</b> and <b>Copy</b> the whole cookie string <i>(everything after 'cookie: ')</i> and <b>Paste</b> here.</li></ol>"; + field.HelpLink = "https://wiki.servarr.com/useful-tools#finding-cookies"; + break; + case "info_useragent": + field.Label = "How to get the User-Agent"; + field.Value = "<ol><li>From the same place you fetched the cookie,</li><li>Find <b>'user-agent:'</b> in the <b>Request Headers</b> section</li><li><b>Select</b> and <b>Copy</b> the whole user-agent string <i>(everything after 'user-agent: ')</i> and <b>Paste</b> here.</li></ol>"; + field.HelpLink = "https://wiki.servarr.com/useful-tools#finding-cookies"; + break; + case "info_flaresolverr": + field.Label = "FlareSolverr Info"; + field.Value = "This site may use Cloudflare DDoS Protection, therefore Prowlarr requires <a href=\"https://wiki.servarr.com/prowlarr/faq#can-i-use-flaresolverr-indexers\" target=\"_blank\" rel=\"noreferrer\">FlareSolverr</a> to access it."; + field.HelpLink = "https://wiki.servarr.com/prowlarr/faq#can-i-use-flaresolverr-indexers"; + break; + case "info_category_8000": + field.Label = $"About {definition.Name} Categories"; + field.Value = $"{definition.Name} does not return categories in its search results. To sync to your apps, include 8000(Other) in your Apps' Sync Categories."; + field.HelpLink = "https://wiki.servarr.com/prowlarr/faq#prowlarr-will-not-sync-x-indexer-to-app"; + break; + } + } else { field.Value = setting.Default; diff --git a/src/Prowlarr.Api.V1/Indexers/NewznabController.cs b/src/Prowlarr.Api.V1/Indexers/NewznabController.cs index 4ee129caf..ae07a7ba7 100644 --- a/src/Prowlarr.Api.V1/Indexers/NewznabController.cs +++ b/src/Prowlarr.Api.V1/Indexers/NewznabController.cs @@ -198,7 +198,9 @@ namespace NzbDrone.Api.V1.Indexers } } - return CreateResponse(results.ToXml(indexer.Protocol)); + var preferMagnetUrl = indexer.Protocol == DownloadProtocol.Torrent && indexerDef.Settings is ITorrentIndexerSettings torrentIndexerSettings && (torrentIndexerSettings.TorrentBaseSettings?.PreferMagnetUrl ?? false); + + return CreateResponse(results.ToXml(indexer.Protocol, preferMagnetUrl)); default: return CreateResponse(CreateErrorXML(202, $"No such function ({requestType})"), statusCode: StatusCodes.Status400BadRequest); } @@ -253,20 +255,25 @@ namespace NzbDrone.Api.V1.Indexers var source = Request.GetSource(); var host = Request.GetHostName(); - var unprotectedlLink = _downloadMappingService.ConvertToNormalLink(link); + var unprotectedLink = _downloadMappingService.ConvertToNormalLink(link); + + if (unprotectedLink.IsNullOrWhiteSpace()) + { + throw new BadRequestException("Failed to normalize provided link"); + } // If Indexer is set to download via Redirect then just redirect to the link if (indexer.SupportsRedirect && indexerDef.Redirect) { - _downloadService.RecordRedirect(unprotectedlLink, id, source, host, file); - return RedirectPermanent(unprotectedlLink); + _downloadService.RecordRedirect(unprotectedLink, id, source, host, file); + return RedirectPermanent(unprotectedLink); } byte[] downloadBytes; try { - downloadBytes = await _downloadService.DownloadReport(unprotectedlLink, id, source, host, file); + downloadBytes = await _downloadService.DownloadReport(unprotectedLink, id, source, host, file); } catch (ReleaseUnavailableException ex) { @@ -334,7 +341,7 @@ namespace NzbDrone.Api.V1.Indexers { var blockedIndexers = _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); - return blockedIndexers.TryGetValue(indexer.Definition.Id, out var blockedIndexerStatus) ? blockedIndexerStatus : null; + return blockedIndexers.GetValueOrDefault(indexer.Definition.Id); } private void AddRetryAfterHeader(int retryAfterSeconds) diff --git a/src/Prowlarr.Api.V1/Logs/LogController.cs b/src/Prowlarr.Api.V1/Logs/LogController.cs index f93f99ba6..18fad89e8 100644 --- a/src/Prowlarr.Api.V1/Logs/LogController.cs +++ b/src/Prowlarr.Api.V1/Logs/LogController.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Instrumentation; using Prowlarr.Http; using Prowlarr.Http.Extensions; @@ -10,18 +13,29 @@ namespace Prowlarr.Api.V1.Logs public class LogController : Controller { private readonly ILogService _logService; + private readonly IConfigFileProvider _configFileProvider; - public LogController(ILogService logService) + public LogController(ILogService logService, IConfigFileProvider configFileProvider) { _logService = logService; + _configFileProvider = configFileProvider; } [HttpGet] [Produces("application/json")] public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string level) { + if (!_configFileProvider.LogDbEnabled) + { + return new PagingResource<LogResource>(); + } + var pagingResource = new PagingResource<LogResource>(paging); - var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(); + var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(new HashSet<string>(StringComparer.OrdinalIgnoreCase) + { + "id", + "time" + }); if (pageSpec.SortKey == "time") { diff --git a/src/Prowlarr.Api.V1/Notifications/NotificationController.cs b/src/Prowlarr.Api.V1/Notifications/NotificationController.cs index b6aa8d99e..1520a8dd4 100644 --- a/src/Prowlarr.Api.V1/Notifications/NotificationController.cs +++ b/src/Prowlarr.Api.V1/Notifications/NotificationController.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Notifications; +using NzbDrone.SignalR; using Prowlarr.Http; namespace Prowlarr.Api.V1.Notifications @@ -11,8 +12,8 @@ namespace Prowlarr.Api.V1.Notifications public static readonly NotificationResourceMapper ResourceMapper = new (); public static readonly NotificationBulkResourceMapper BulkResourceMapper = new (); - public NotificationController(NotificationFactory notificationFactory) - : base(notificationFactory, "notification", ResourceMapper, BulkResourceMapper) + public NotificationController(IBroadcastSignalRMessage signalRBroadcaster, NotificationFactory notificationFactory) + : base(signalRBroadcaster, notificationFactory, "notification", ResourceMapper, BulkResourceMapper) { } diff --git a/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs b/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs index 1951866ac..dcac3f6d4 100644 --- a/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs +++ b/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs @@ -43,14 +43,14 @@ namespace Prowlarr.Api.V1.Notifications return resource; } - public override NotificationDefinition ToModel(NotificationResource resource) + public override NotificationDefinition ToModel(NotificationResource resource, NotificationDefinition existingDefinition) { if (resource == null) { return default(NotificationDefinition); } - var definition = base.ToModel(resource); + var definition = base.ToModel(resource, existingDefinition); definition.OnGrab = resource.OnGrab; definition.SupportsOnGrab = resource.SupportsOnGrab; diff --git a/src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs b/src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs index 180bc5809..2a473e4bb 100644 --- a/src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs +++ b/src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs @@ -23,7 +23,7 @@ namespace Prowlarr.Api.V1.Profiles.App [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<AppProfileResource> Create(AppProfileResource resource) + public ActionResult<AppProfileResource> Create([FromBody] AppProfileResource resource) { var model = resource.ToModel(); model = _appProfileService.Add(model); @@ -41,7 +41,7 @@ namespace Prowlarr.Api.V1.Profiles.App [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<AppProfileResource> Update(AppProfileResource resource) + public ActionResult<AppProfileResource> Update([FromBody] AppProfileResource resource) { var model = resource.ToModel(); diff --git a/src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs b/src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs index aa88dfac6..623b6bf65 100644 --- a/src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs +++ b/src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs @@ -9,8 +9,8 @@ namespace Prowlarr.Api.V1.Profiles.App { public string Name { get; set; } public bool EnableRss { get; set; } - public bool EnableInteractiveSearch { get; set; } public bool EnableAutomaticSearch { get; set; } + public bool EnableInteractiveSearch { get; set; } public int MinimumSeeders { get; set; } } @@ -28,8 +28,8 @@ namespace Prowlarr.Api.V1.Profiles.App Id = model.Id, Name = model.Name, EnableRss = model.EnableRss, - EnableInteractiveSearch = model.EnableInteractiveSearch, EnableAutomaticSearch = model.EnableAutomaticSearch, + EnableInteractiveSearch = model.EnableInteractiveSearch, MinimumSeeders = model.MinimumSeeders }; } @@ -46,8 +46,8 @@ namespace Prowlarr.Api.V1.Profiles.App Id = resource.Id, Name = resource.Name, EnableRss = resource.EnableRss, - EnableInteractiveSearch = resource.EnableInteractiveSearch, EnableAutomaticSearch = resource.EnableAutomaticSearch, + EnableInteractiveSearch = resource.EnableInteractiveSearch, MinimumSeeders = resource.MinimumSeeders }; } diff --git a/src/Prowlarr.Api.V1/ProviderControllerBase.cs b/src/Prowlarr.Api.V1/ProviderControllerBase.cs index 809697a06..725f6e58b 100644 --- a/src/Prowlarr.Api.V1/ProviderControllerBase.cs +++ b/src/Prowlarr.Api.V1/ProviderControllerBase.cs @@ -3,14 +3,22 @@ using System.Linq; using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.Validation; using NzbDrone.Http.REST.Attributes; +using NzbDrone.SignalR; using Prowlarr.Http.REST; namespace Prowlarr.Api.V1 { - public abstract class ProviderControllerBase<TProviderResource, TBulkProviderResource, TProvider, TProviderDefinition> : RestController<TProviderResource> + public abstract class ProviderControllerBase<TProviderResource, TBulkProviderResource, TProvider, TProviderDefinition> : RestControllerWithSignalR<TProviderResource, TProviderDefinition>, + IHandle<ProviderAddedEvent<TProvider>>, + IHandle<ProviderUpdatedEvent<TProvider>>, + IHandle<ProviderDeletedEvent<TProvider>> where TProviderDefinition : ProviderDefinition, new() where TBulkProviderResource : ProviderBulkResource<TBulkProviderResource>, new() where TProvider : IProvider @@ -20,18 +28,20 @@ namespace Prowlarr.Api.V1 protected readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper; private readonly ProviderBulkResourceMapper<TBulkProviderResource, TProviderDefinition> _bulkResourceMapper; - protected ProviderControllerBase(IProviderFactory<TProvider, + protected ProviderControllerBase(IBroadcastSignalRMessage signalRBroadcaster, + IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper, ProviderBulkResourceMapper<TBulkProviderResource, TProviderDefinition> bulkResourceMapper) + : base(signalRBroadcaster) { _providerFactory = providerFactory; _resourceMapper = resourceMapper; _bulkResourceMapper = bulkResourceMapper; SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name.EqualsIgnoreCase(c) && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); @@ -142,7 +152,8 @@ namespace Prowlarr.Api.V1 private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate) { - var definition = _resourceMapper.ToModel(providerResource); + var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; + var definition = _resourceMapper.ToModel(providerResource, existingDefinition); if (validate && (definition.Enable || forceValidate)) { @@ -196,9 +207,9 @@ namespace Prowlarr.Api.V1 [SkipValidation(true, false)] [HttpPost("test")] [Consumes("application/json")] - public object Test([FromBody] TProviderResource providerResource) + public object Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false) { - var providerDefinition = GetDefinition(providerResource, true, true, true); + var providerDefinition = GetDefinition(providerResource, true, !forceTest, true); Test(providerDefinition, true); @@ -231,7 +242,7 @@ namespace Prowlarr.Api.V1 [HttpPost("action/{name}")] [Consumes("application/json")] [Produces("application/json")] - public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) + public IActionResult RequestAction([FromRoute] string name, [FromBody] TProviderResource resource) { var providerDefinition = GetDefinition(resource, false, false, false); @@ -242,6 +253,24 @@ namespace Prowlarr.Api.V1 return Json(data); } + [NonAction] + public void Handle(ProviderAddedEvent<TProvider> message) + { + BroadcastResourceChange(ModelAction.Created, message.Definition.Id); + } + + [NonAction] + public void Handle(ProviderUpdatedEvent<TProvider> message) + { + BroadcastResourceChange(ModelAction.Updated, message.Definition.Id); + } + + [NonAction] + public void Handle(ProviderDeletedEvent<TProvider> message) + { + BroadcastResourceChange(ModelAction.Deleted, message.ProviderId); + } + private void Validate(TProviderDefinition definition, bool includeWarnings) { var validationResult = definition.Settings.Validate(); diff --git a/src/Prowlarr.Api.V1/ProviderResource.cs b/src/Prowlarr.Api.V1/ProviderResource.cs index f39294f28..3de6725c7 100644 --- a/src/Prowlarr.Api.V1/ProviderResource.cs +++ b/src/Prowlarr.Api.V1/ProviderResource.cs @@ -45,7 +45,7 @@ namespace Prowlarr.Api.V1 }; } - public virtual TProviderDefinition ToModel(TProviderResource resource) + public virtual TProviderDefinition ToModel(TProviderResource resource, TProviderDefinition existingDefinition) { if (resource == null) { @@ -65,7 +65,7 @@ namespace Prowlarr.Api.V1 }; var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); - definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract, existingDefinition?.Settings); return definition; } diff --git a/src/Prowlarr.Api.V1/Prowlarr.Api.V1.csproj b/src/Prowlarr.Api.V1/Prowlarr.Api.V1.csproj index 49878d538..15684b0fe 100644 --- a/src/Prowlarr.Api.V1/Prowlarr.Api.V1.csproj +++ b/src/Prowlarr.Api.V1/Prowlarr.Api.V1.csproj @@ -4,7 +4,8 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="NLog" Version="5.2.0" /> + <PackageReference Include="NLog" Version="5.4.0" /> + <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" /> diff --git a/src/Prowlarr.Api.V1/Search/ReleaseResource.cs b/src/Prowlarr.Api.V1/Search/ReleaseResource.cs index 49364f445..50cf66bd1 100644 --- a/src/Prowlarr.Api.V1/Search/ReleaseResource.cs +++ b/src/Prowlarr.Api.V1/Search/ReleaseResource.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -23,8 +24,10 @@ namespace Prowlarr.Api.V1.Search public string ReleaseHash { get; set; } public string Title { get; set; } public string SortTitle { get; set; } - public bool Approved { get; set; } public int ImdbId { get; set; } + public int TmdbId { get; set; } + public int TvdbId { get; set; } + public int TvMazeId { get; set; } public DateTime PublishDate { get; set; } public string CommentUrl { get; set; } public string DownloadUrl { get; set; } @@ -53,6 +56,9 @@ namespace Prowlarr.Api.V1.Search return $"{Title}{extension}"; } } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int? DownloadClientId { get; set; } } public static class ReleaseResourceMapper @@ -80,6 +86,9 @@ namespace Prowlarr.Api.V1.Search Title = releaseInfo.Title, SortTitle = releaseInfo.Title.NormalizeTitle(), ImdbId = releaseInfo.ImdbId, + TmdbId = releaseInfo.TmdbId, + TvdbId = releaseInfo.TvdbId, + TvMazeId = releaseInfo.TvMazeId, PublishDate = releaseInfo.PublishDate, CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, diff --git a/src/Prowlarr.Api.V1/Search/SearchController.cs b/src/Prowlarr.Api.V1/Search/SearchController.cs index bc421f876..c73ae00f3 100644 --- a/src/Prowlarr.Api.V1/Search/SearchController.cs +++ b/src/Prowlarr.Api.V1/Search/SearchController.cs @@ -9,6 +9,7 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch; @@ -54,7 +55,7 @@ namespace Prowlarr.Api.V1.Search [HttpPost] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<ReleaseResource> GrabRelease(ReleaseResource release) + public async Task<ActionResult<ReleaseResource>> GrabRelease([FromBody] ReleaseResource release) { ValidateResource(release); @@ -73,11 +74,12 @@ namespace Prowlarr.Api.V1.Search try { - _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null).GetAwaiter().GetResult(); + await _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, release.DownloadClientId); } catch (ReleaseDownloadException ex) { _logger.Error(ex, "Getting release from indexer failed"); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); } @@ -87,12 +89,16 @@ namespace Prowlarr.Api.V1.Search [HttpPost("bulk")] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<ReleaseResource> GrabReleases(List<ReleaseResource> releases) + public async Task<ActionResult<ReleaseResource>> GrabReleases([FromBody] List<ReleaseResource> releases) { + releases.ForEach(release => ValidateResource(release)); + var source = Request.GetSource(); var host = Request.GetHostName(); - var groupedReleases = releases.GroupBy(r => r.IndexerId); + var grabbedReleases = new List<ReleaseResource>(); + + var groupedReleases = releases.GroupBy(r => r.IndexerId).ToList(); foreach (var indexerReleases in groupedReleases) { @@ -100,22 +106,42 @@ namespace Prowlarr.Api.V1.Search foreach (var release in indexerReleases) { - ValidateResource(release); - var releaseInfo = _remoteReleaseCache.Find(GetCacheKey(release)); + if (releaseInfo == null) + { + _logger.Error("Couldn't find requested release in cache, cache timeout probably expired."); + + continue; + } + try { - _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null).GetAwaiter().GetResult(); + await _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null); } catch (ReleaseDownloadException ex) { _logger.Error(ex, "Getting release from indexer failed"); + + continue; } + catch (DownloadClientException ex) + { + _logger.Error(ex, "Failed to send grabbed release to download client"); + + continue; + } + + grabbedReleases.Add(release); } } - return Ok(releases); + if (!grabbedReleases.Any()) + { + throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Failed to grab any release"); + } + + return Ok(grabbedReleases); } [HttpGet] diff --git a/src/Prowlarr.Api.V1/System/Backup/BackupController.cs b/src/Prowlarr.Api.V1/System/Backup/BackupController.cs index 927026805..b2c7261b7 100644 --- a/src/Prowlarr.Api.V1/System/Backup/BackupController.cs +++ b/src/Prowlarr.Api.V1/System/Backup/BackupController.cs @@ -51,7 +51,7 @@ namespace Prowlarr.Api.V1.System.Backup } [RestDeleteById] - public void DeleteBackup(int id) + public object DeleteBackup(int id) { var backup = GetBackup(id); @@ -68,6 +68,8 @@ namespace Prowlarr.Api.V1.System.Backup } _diskProvider.DeleteFile(path); + + return new { }; } [HttpPost("restore/{id:int}")] diff --git a/src/Prowlarr.Api.V1/Tags/TagController.cs b/src/Prowlarr.Api.V1/Tags/TagController.cs index 740fb9741..83e9825c9 100644 --- a/src/Prowlarr.Api.V1/Tags/TagController.cs +++ b/src/Prowlarr.Api.V1/Tags/TagController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; @@ -20,6 +21,8 @@ namespace Prowlarr.Api.V1.Tags : base(signalRBroadcaster) { _tagService = tagService; + + SharedValidator.RuleFor(c => c.Label).NotEmpty(); } public override TagResource GetResourceById(int id) @@ -37,7 +40,7 @@ namespace Prowlarr.Api.V1.Tags [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<TagResource> Create(TagResource resource) + public ActionResult<TagResource> Create([FromBody] TagResource resource) { return Created(_tagService.Add(resource.ToModel()).Id); } @@ -45,7 +48,7 @@ namespace Prowlarr.Api.V1.Tags [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<TagResource> Update(TagResource resource) + public ActionResult<TagResource> Update([FromBody] TagResource resource) { _tagService.Update(resource.ToModel()); return Accepted(resource.Id); diff --git a/src/Prowlarr.Api.V1/Update/UpdateController.cs b/src/Prowlarr.Api.V1/Update/UpdateController.cs index 077a6f6f8..eac25fccf 100644 --- a/src/Prowlarr.Api.V1/Update/UpdateController.cs +++ b/src/Prowlarr.Api.V1/Update/UpdateController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Update.History; using Prowlarr.Http; @@ -13,11 +14,13 @@ namespace Prowlarr.Api.V1.Update { private readonly IRecentUpdateProvider _recentUpdateProvider; private readonly IUpdateHistoryService _updateHistoryService; + private readonly IConfigFileProvider _configFileProvider; - public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) + public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService, IConfigFileProvider configFileProvider) { _recentUpdateProvider = recentUpdateProvider; _updateHistoryService = updateHistoryService; + _configFileProvider = configFileProvider; } [HttpGet] @@ -45,6 +48,11 @@ namespace Prowlarr.Api.V1.Update installed.Installed = true; } + if (!_configFileProvider.LogDbEnabled) + { + return resources; + } + var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate) .DistinctBy(v => v.Version) .ToDictionary(v => v.Version); diff --git a/src/Prowlarr.Api.V1/openapi.json b/src/Prowlarr.Api.V1/openapi.json index 74ff899ca..35cb7d6a0 100644 --- a/src/Prowlarr.Api.V1/openapi.json +++ b/src/Prowlarr.Api.V1/openapi.json @@ -34,7 +34,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -64,7 +64,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -108,7 +108,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -136,7 +136,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -148,7 +148,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -187,7 +187,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -215,7 +215,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -241,7 +241,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -253,7 +253,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -273,6 +273,16 @@ "tags": [ "Application" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -284,7 +294,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -296,7 +306,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -327,7 +337,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -348,7 +358,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -365,7 +375,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -398,7 +408,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -427,7 +437,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -455,7 +465,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -478,7 +488,7 @@ } }, "500": { - "description": "Server Error" + "description": "Internal Server Error" } } } @@ -490,7 +500,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -549,7 +559,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } }, @@ -559,7 +569,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -571,7 +581,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -583,7 +593,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -616,7 +626,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -639,7 +649,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -651,7 +661,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -674,7 +684,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -702,7 +712,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -723,7 +733,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -740,7 +750,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -773,7 +783,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -809,7 +819,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -837,7 +847,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -849,7 +859,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -878,7 +888,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -916,7 +926,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -944,7 +954,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -963,7 +973,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -993,7 +1003,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1037,7 +1047,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1065,7 +1075,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1077,7 +1087,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1116,7 +1126,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1144,7 +1154,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1170,7 +1180,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1182,7 +1192,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1202,6 +1212,16 @@ "tags": [ "DownloadClient" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -1213,7 +1233,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1225,7 +1245,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1256,7 +1276,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1279,7 +1299,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1315,7 +1335,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1334,7 +1354,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1378,7 +1398,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1399,7 +1419,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1411,7 +1431,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1468,8 +1488,11 @@ "name": "eventType", "in": "query", "schema": { - "type": "integer", - "format": "int32" + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } } }, { @@ -1485,11 +1508,22 @@ "schema": { "type": "string" } + }, + { + "name": "indexerIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } } ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1525,7 +1559,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1572,7 +1606,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1605,7 +1639,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1641,7 +1675,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1660,7 +1694,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1690,7 +1724,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1734,7 +1768,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1762,7 +1796,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1774,7 +1808,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1813,7 +1847,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1841,7 +1875,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1867,7 +1901,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1879,7 +1913,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1899,6 +1933,16 @@ "tags": [ "Indexer" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -1910,7 +1954,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1922,7 +1966,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1953,7 +1997,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -1965,7 +2009,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -1998,7 +2042,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2042,7 +2086,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2070,7 +2114,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2082,7 +2126,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2121,7 +2165,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2140,7 +2184,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2160,6 +2204,16 @@ "tags": [ "IndexerProxy" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -2171,7 +2225,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2183,7 +2237,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2214,7 +2268,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2265,7 +2319,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2284,7 +2338,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2306,7 +2360,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2318,7 +2372,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2381,7 +2435,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2400,7 +2454,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -2433,7 +2487,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -2701,7 +2755,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2962,7 +3016,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -2999,7 +3053,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3036,7 +3090,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3059,7 +3113,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3103,7 +3157,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3131,7 +3185,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3143,7 +3197,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3182,7 +3236,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3201,7 +3255,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3221,6 +3275,16 @@ "tags": [ "Notification" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -3232,7 +3296,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3244,7 +3308,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3275,7 +3339,7 @@ }, "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3287,7 +3351,24 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResource" + } + } + } + } + } + }, + "head": { + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { @@ -3315,7 +3396,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3386,7 +3467,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3420,7 +3501,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3450,7 +3531,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3472,7 +3553,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3495,7 +3576,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3507,7 +3588,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3526,7 +3607,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3538,7 +3619,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3550,7 +3631,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3562,7 +3643,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3585,7 +3666,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3621,7 +3702,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3649,7 +3730,7 @@ ], "responses": { "200": { - "description": "Success" + "description": "OK" } } } @@ -3661,7 +3742,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3690,7 +3771,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3720,7 +3801,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3739,7 +3820,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3761,7 +3842,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3794,7 +3875,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3832,7 +3913,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3860,7 +3941,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3879,7 +3960,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3898,7 +3979,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3920,7 +4001,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -3953,7 +4034,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -3999,10 +4080,10 @@ "enableRss": { "type": "boolean" }, - "enableInteractiveSearch": { + "enableAutomaticSearch": { "type": "boolean" }, - "enableAutomaticSearch": { + "enableInteractiveSearch": { "type": "boolean" }, "minimumSeeders": { @@ -4297,7 +4378,9 @@ "nullable": true }, "duration": { - "$ref": "#/components/schemas/TimeSpan" + "type": "string", + "format": "date-span", + "nullable": true }, "exception": { "type": "string", @@ -4615,6 +4698,9 @@ "type": "string", "nullable": true }, + "privacy": { + "$ref": "#/components/schemas/PrivacyLevel" + }, "placeholder": { "type": "string", "nullable": true @@ -4653,7 +4739,8 @@ "nullable": true }, "wikiUrl": { - "$ref": "#/components/schemas/HttpUri" + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -4787,6 +4874,10 @@ "type": "string", "nullable": true }, + "logSizeLimit": { + "type": "integer", + "format": "int32" + }, "consoleLogLevel": { "type": "string", "nullable": true @@ -4876,6 +4967,9 @@ "historyCleanupDays": { "type": "integer", "format": "int32" + }, + "trustCgnatIpAddresses": { + "type": "boolean" } }, "additionalProperties": false @@ -4898,48 +4992,6 @@ }, "additionalProperties": false }, - "HttpUri": { - "type": "object", - "properties": { - "fullUri": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "scheme": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "host": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "port": { - "type": "integer", - "format": "int32", - "nullable": true, - "readOnly": true - }, - "path": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "query": { - "type": "string", - "nullable": true, - "readOnly": true - }, - "fragment": { - "type": "string", - "nullable": true, - "readOnly": true - } - }, - "additionalProperties": false - }, "IActionResult": { "type": "object", "additionalProperties": false @@ -4999,6 +5051,10 @@ "type": "integer", "format": "int32", "nullable": true + }, + "preferMagnetUrl": { + "type": "boolean", + "nullable": true } }, "additionalProperties": false @@ -5326,6 +5382,10 @@ "type": "integer", "format": "int32" }, + "averageGrabResponseTime": { + "type": "integer", + "format": "int32" + }, "numberOfQueries": { "type": "integer", "format": "int32" @@ -5661,6 +5721,15 @@ }, "additionalProperties": false }, + "PrivacyLevel": { + "enum": [ + "normal", + "password", + "apiKey", + "userName" + ], + "type": "string" + }, "ProviderMessage": { "type": "object", "properties": { @@ -5751,13 +5820,22 @@ "type": "string", "nullable": true }, - "approved": { - "type": "boolean" - }, "imdbId": { "type": "integer", "format": "int32" }, + "tmdbId": { + "type": "integer", + "format": "int32" + }, + "tvdbId": { + "type": "integer", + "format": "int32" + }, + "tvMazeId": { + "type": "integer", + "format": "int32" + }, "publishDate": { "type": "string", "format": "date-time" @@ -5817,6 +5895,11 @@ "type": "string", "nullable": true, "readOnly": true + }, + "downloadClientId": { + "type": "integer", + "format": "int32", + "nullable": true } }, "additionalProperties": false @@ -5943,7 +6026,8 @@ "$ref": "#/components/schemas/DatabaseType" }, "databaseVersion": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "authentication": { "$ref": "#/components/schemas/AuthenticationType" @@ -5957,7 +6041,8 @@ "nullable": true }, "runtimeVersion": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "runtimeName": { "type": "string", @@ -6077,66 +6162,8 @@ "format": "date-time" }, "lastDuration": { - "$ref": "#/components/schemas/TimeSpan" - } - }, - "additionalProperties": false - }, - "TimeSpan": { - "type": "object", - "properties": { - "ticks": { - "type": "integer", - "format": "int64" - }, - "days": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "hours": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "milliseconds": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minutes": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "seconds": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "totalDays": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalHours": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalMilliseconds": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalMinutes": { - "type": "number", - "format": "double", - "readOnly": true - }, - "totalSeconds": { - "type": "number", - "format": "double", + "type": "string", + "format": "date-span", "readOnly": true } }, @@ -6241,7 +6268,8 @@ "format": "int32" }, "version": { - "$ref": "#/components/schemas/Version" + "type": "string", + "nullable": true }, "branch": { "type": "string", @@ -6300,42 +6328,6 @@ } }, "additionalProperties": false - }, - "Version": { - "type": "object", - "properties": { - "major": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minor": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "build": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "revision": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "majorRevision": { - "type": "integer", - "format": "int32", - "readOnly": true - }, - "minorRevision": { - "type": "integer", - "format": "int32", - "readOnly": true - } - }, - "additionalProperties": false } }, "securitySchemes": { @@ -6347,7 +6339,7 @@ }, "apikey": { "type": "apiKey", - "description": "Apikey passed as header", + "description": "Apikey passed as query parameter", "name": "apikey", "in": "query" } diff --git a/src/Prowlarr.Benchmark.Test/Prowlarr.Benchmark.Test.csproj b/src/Prowlarr.Benchmark.Test/Prowlarr.Benchmark.Test.csproj index 8935c3616..9bbb577cb 100644 --- a/src/Prowlarr.Benchmark.Test/Prowlarr.Benchmark.Test.csproj +++ b/src/Prowlarr.Benchmark.Test/Prowlarr.Benchmark.Test.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="BenchmarkDotNet" Version="0.13.5" /> + <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> </ItemGroup> <ItemGroup> diff --git a/src/Prowlarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Prowlarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 9a9c54627..318f22929 100644 --- a/src/Prowlarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Prowlarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -1,12 +1,18 @@ using System; +using System.Text.RegularExpressions; +using Diacritical; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; namespace Prowlarr.Http.Authentication { public static class AuthenticationBuilderExtensions { + private static readonly Regex CookieNameRegex = new Regex(@"[^a-z0-9]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action<ApiKeyAuthenticationOptions> options) { return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(name, options); @@ -29,19 +35,27 @@ namespace Prowlarr.Http.Authentication public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) { - return services.AddAuthentication() - .AddNone(AuthenticationType.None.ToString()) - .AddExternal(AuthenticationType.External.ToString()) - .AddBasic(AuthenticationType.Basic.ToString()) - .AddCookie(AuthenticationType.Forms.ToString(), options => + services.AddOptions<CookieAuthenticationOptions>(AuthenticationType.Forms.ToString()) + .Configure<IConfigFileProvider>((options, configFileProvider) => { - options.Cookie.Name = "ProwlarrAuth"; + // Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names + var instanceName = configFileProvider.InstanceName; + instanceName = instanceName.RemoveDiacritics(); + instanceName = CookieNameRegex.Replace(instanceName, string.Empty); + + options.Cookie.Name = $"{instanceName}Auth"; options.AccessDeniedPath = "/login?loginFailed=true"; options.LoginPath = "/login"; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.SlidingExpiration = true; options.ReturnUrlParameter = "returnUrl"; - }) + }); + + return services.AddAuthentication() + .AddNone(AuthenticationType.None.ToString()) + .AddExternal(AuthenticationType.External.ToString()) + .AddBasic(AuthenticationType.Basic.ToString()) + .AddCookie(AuthenticationType.Forms.ToString()) .AddApiKey("API", options => { options.HeaderName = "X-Api-Key"; diff --git a/src/Prowlarr.Http/Authentication/AuthenticationController.cs b/src/Prowlarr.Http/Authentication/AuthenticationController.cs index 1d2de1d89..05b058895 100644 --- a/src/Prowlarr.Http/Authentication/AuthenticationController.cs +++ b/src/Prowlarr.Http/Authentication/AuthenticationController.cs @@ -1,9 +1,15 @@ using System.Collections.Generic; +using System.IO; using System.Security.Claims; +using System.Security.Cryptography; using System.Threading.Tasks; +using System.Xml; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -15,11 +21,15 @@ namespace Prowlarr.Http.Authentication { private readonly IAuthenticationService _authService; private readonly IConfigFileProvider _configFileProvider; + private readonly IAppFolderInfo _appFolderInfo; + private readonly Logger _logger; - public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider) + public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider, IAppFolderInfo appFolderInfo, Logger logger) { _authService = authService; _configFileProvider = configFileProvider; + _appFolderInfo = appFolderInfo; + _logger = logger; } [HttpPost("login")] @@ -44,9 +54,35 @@ namespace Prowlarr.Http.Authentication IsPersistent = resource.RememberMe == "on" }; - await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); + try + { + await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); + } + catch (CryptographicException e) + { + if (e.InnerException is XmlException) + { + _logger.Error(e, "Failed to authenticate user due to corrupt XML. Please remove all XML files from {0} and restart Prowlarr", Path.Combine(_appFolderInfo.AppDataFolder, "asp")); + } + else + { + _logger.Error(e, "Failed to authenticate user. {0}", e.Message); + } - return Redirect(_configFileProvider.UrlBase + "/"); + return Unauthorized(); + } + + if (returnUrl.IsNullOrWhiteSpace()) + { + return Redirect(_configFileProvider.UrlBase + "/"); + } + + if (_configFileProvider.UrlBase.IsNullOrWhiteSpace() || returnUrl.StartsWith(_configFileProvider.UrlBase)) + { + return Redirect(returnUrl); + } + + return Redirect(_configFileProvider.UrlBase + returnUrl); } [HttpGet("logout")] diff --git a/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs b/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs index 738364748..7ec3b5220 100644 --- a/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs +++ b/src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs @@ -27,10 +27,13 @@ namespace Prowlarr.Http.Authentication if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses) { if (context.Resource is HttpContext httpContext && - IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) && - ipAddress.IsLocalAddress()) + IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress)) { - context.Succeed(requirement); + if (ipAddress.IsLocalAddress() || + (_configService.TrustCgnatIpAddresses && ipAddress.IsCgnatIpAddress())) + { + context.Succeed(requirement); + } } } diff --git a/src/Prowlarr.Http/ClientSchema/Field.cs b/src/Prowlarr.Http/ClientSchema/Field.cs index 7ceed1ae9..1af56b6d0 100644 --- a/src/Prowlarr.Http/ClientSchema/Field.cs +++ b/src/Prowlarr.Http/ClientSchema/Field.cs @@ -16,10 +16,10 @@ namespace Prowlarr.Http.ClientSchema public string Type { get; set; } public bool Advanced { get; set; } public List<SelectOption> SelectOptions { get; set; } - public string SelectOptionsProviderAction { get; set; } public string Section { get; set; } public string Hidden { get; set; } + public PrivacyLevel Privacy { get; set; } public string Placeholder { get; set; } public bool IsFloat { get; set; } diff --git a/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs b/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs index 171c9db04..959b5357b 100644 --- a/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Prowlarr.Http/ClientSchema/SchemaBuilder.cs @@ -3,17 +3,26 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; +using DryIoc; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Localization; namespace Prowlarr.Http.ClientSchema { public static class SchemaBuilder { + private const string PRIVATE_VALUE = "********"; private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>(); + private static ILocalizationService _localizationService; + + public static void Initialize(IContainer container) + { + _localizationService = container.Resolve<ILocalizationService>(); + } public static List<Field> ToSchema(object model) { @@ -28,13 +37,19 @@ namespace Prowlarr.Http.ClientSchema var field = mapping.Field.Clone(); field.Value = mapping.GetterFunc(model); + if (field.Value != null && !field.Value.Equals(string.Empty) && + (field.Privacy == PrivacyLevel.ApiKey || field.Privacy == PrivacyLevel.Password)) + { + field.Value = PRIVATE_VALUE; + } + result.Add(field); } return result.OrderBy(r => r.Order).ToList(); } - public static object ReadFromSchema(List<Field> fields, Type targetType) + public static object ReadFromSchema(List<Field> fields, Type targetType, object model) { Ensure.That(targetType, () => targetType).IsNotNull(); @@ -49,18 +64,25 @@ namespace Prowlarr.Http.ClientSchema if (field != null) { - mapping.SetterFunc(target, field.Value); + // Use the Privacy property from the mapping's field as Privacy may not be set in the API request (nor is it required) + if ((mapping.Field.Privacy == PrivacyLevel.ApiKey || mapping.Field.Privacy == PrivacyLevel.Password) && + (field.Value?.ToString()?.Equals(PRIVATE_VALUE) ?? false) && + model != null) + { + var existingValue = mapping.GetterFunc(model); + + mapping.SetterFunc(target, existingValue); + } + else + { + mapping.SetterFunc(target, field.Value); + } } } return target; } - public static T ReadFromSchema<T>(List<Field> fields) - { - return (T)ReadFromSchema(fields, typeof(T)); - } - // Ideally this function should begin a System.Linq.Expression expression tree since it's faster. // But it's probably not needed till performance issues pop up. public static FieldMapping[] GetFieldMappings(Type type) @@ -93,18 +115,33 @@ namespace Prowlarr.Http.ClientSchema if (propertyInfo.PropertyType.IsSimpleType()) { var fieldAttribute = property.Item2; + + var label = fieldAttribute.Label.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.Label, + GetTokens(type, fieldAttribute.Label, TokenField.Label)) + : fieldAttribute.Label; + var helpText = fieldAttribute.HelpText.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.HelpText, + GetTokens(type, fieldAttribute.Label, TokenField.HelpText)) + : fieldAttribute.HelpText; + var helpTextWarning = fieldAttribute.HelpTextWarning.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.HelpTextWarning, + GetTokens(type, fieldAttribute.Label, TokenField.HelpTextWarning)) + : fieldAttribute.HelpTextWarning; + var field = new Field { Name = prefix + GetCamelCaseName(propertyInfo.Name), - Label = fieldAttribute.Label, + Label = label, Unit = fieldAttribute.Unit, - HelpText = fieldAttribute.HelpText, - HelpTextWarning = fieldAttribute.HelpTextWarning, + HelpText = helpText, + HelpTextWarning = helpTextWarning, HelpLink = fieldAttribute.HelpLink, Order = fieldAttribute.Order, Advanced = fieldAttribute.Advanced, Type = fieldAttribute.Type.ToString().FirstCharToLower(), Section = fieldAttribute.Section, + Privacy = fieldAttribute.Privacy, Placeholder = fieldAttribute.Placeholder }; @@ -125,7 +162,7 @@ namespace Prowlarr.Http.ClientSchema field.Hidden = fieldAttribute.Hidden.ToString().FirstCharToLower(); } - if (fieldAttribute.Type is FieldType.Number && propertyInfo.PropertyType == typeof(double)) + if (fieldAttribute.Type is FieldType.Number && (propertyInfo.PropertyType == typeof(double) || propertyInfo.PropertyType == typeof(double?))) { field.IsFloat = true; } @@ -137,7 +174,7 @@ namespace Prowlarr.Http.ClientSchema Field = field, PropertyType = propertyInfo.PropertyType, GetterFunc = t => propertyInfo.GetValue(targetSelector(t), null), - SetterFunc = (t, v) => propertyInfo.SetValue(targetSelector(t), valueConverter(v), null) + SetterFunc = (t, v) => propertyInfo.SetValue(targetSelector(t), v?.GetType() == propertyInfo.PropertyType ? v : valueConverter(v), null) }); } else @@ -158,6 +195,24 @@ namespace Prowlarr.Http.ClientSchema .ToArray(); } + private static Dictionary<string, object> GetTokens(Type type, string label, TokenField field) + { + var tokens = new Dictionary<string, object>(); + + foreach (var propertyInfo in type.GetProperties()) + { + foreach (var attribute in propertyInfo.GetCustomAttributes(true)) + { + if (attribute is FieldTokenAttribute fieldTokenAttribute && fieldTokenAttribute.Field == field && fieldTokenAttribute.Label == label) + { + tokens.Add(fieldTokenAttribute.Token, fieldTokenAttribute.Value); + } + } + } + + return tokens; + } + private static List<SelectOption> GetSelectOptions(Type selectOptions) { if (selectOptions.IsEnum) diff --git a/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs index d17e9ebc7..becd6a7db 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -39,7 +39,7 @@ namespace Prowlarr.Http.Frontend.Mappers return stream; } - protected string GetHtmlText() + protected virtual string GetHtmlText() { if (RuntimeInfo.IsProduction && _generatedContent != null) { diff --git a/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index 347c02876..112ad9383 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -9,6 +9,8 @@ namespace Prowlarr.Http.Frontend.Mappers { public class LoginHtmlMapper : HtmlMapperBase { + private readonly IConfigFileProvider _configFileProvider; + public LoginHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, @@ -16,6 +18,7 @@ namespace Prowlarr.Http.Frontend.Mappers Logger logger) : base(diskProvider, cacheBreakProviderFactory, logger) { + _configFileProvider = configFileProvider; HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); UrlBase = configFileProvider.UrlBase; } @@ -29,5 +32,15 @@ namespace Prowlarr.Http.Frontend.Mappers { return resourceUrl.StartsWith("/login"); } + + protected override string GetHtmlText() + { + var html = base.GetHtmlText(); + var theme = _configFileProvider.Theme; + + html = html.Replace("_THEME_", theme); + + return html; + } } } diff --git a/src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs b/src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs index 4cd37c24e..cf7e7b5cf 100644 --- a/src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs +++ b/src/Prowlarr.Http/Middleware/UrlBaseMiddleware.cs @@ -20,6 +20,8 @@ namespace Prowlarr.Http.Middleware if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace()) { context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}"); + context.Response.StatusCode = 307; + return; } diff --git a/src/Prowlarr.Http/PagingResource.cs b/src/Prowlarr.Http/PagingResource.cs index 97907cd66..a442e812e 100644 --- a/src/Prowlarr.Http/PagingResource.cs +++ b/src/Prowlarr.Http/PagingResource.cs @@ -21,7 +21,7 @@ namespace Prowlarr.Http public string SortKey { get; set; } public SortDirection SortDirection { get; set; } public int TotalRecords { get; set; } - public List<TResource> Records { get; set; } + public List<TResource> Records { get; set; } = new (); public PagingResource() { @@ -38,7 +38,11 @@ namespace Prowlarr.Http public static class PagingResourceMapper { - public static PagingSpec<TModel> MapToPagingSpec<TResource, TModel>(this PagingResource<TResource> pagingResource, string defaultSortKey = "Id", SortDirection defaultSortDirection = SortDirection.Ascending) + public static PagingSpec<TModel> MapToPagingSpec<TResource, TModel>( + this PagingResource<TResource> pagingResource, + HashSet<string> allowedSortKeys, + string defaultSortKey = "id", + SortDirection defaultSortDirection = SortDirection.Ascending) { var pagingSpec = new PagingSpec<TModel> { @@ -48,14 +52,15 @@ namespace Prowlarr.Http SortDirection = pagingResource.SortDirection, }; - if (pagingResource.SortKey == null) - { - pagingSpec.SortKey = defaultSortKey; - if (pagingResource.SortDirection == SortDirection.Default) - { - pagingSpec.SortDirection = defaultSortDirection; - } - } + pagingSpec.SortKey = pagingResource.SortKey != null && + allowedSortKeys is { Count: > 0 } && + allowedSortKeys.Contains(pagingResource.SortKey) + ? pagingResource.SortKey + : defaultSortKey; + + pagingSpec.SortDirection = pagingResource.SortDirection == SortDirection.Default + ? defaultSortDirection + : pagingResource.SortDirection; return pagingSpec; } diff --git a/src/Prowlarr.Http/Ping/PingController.cs b/src/Prowlarr.Http/Ping/PingController.cs index d36f958a1..776e8e58a 100644 --- a/src/Prowlarr.Http/Ping/PingController.cs +++ b/src/Prowlarr.Http/Ping/PingController.cs @@ -22,6 +22,7 @@ namespace Prowlarr.Http [AllowAnonymous] [HttpGet("/ping")] + [HttpHead("/ping")] [Produces("application/json")] public ActionResult<PingResource> GetStatus() { diff --git a/src/Prowlarr.Http/Prowlarr.Http.csproj b/src/Prowlarr.Http/Prowlarr.Http.csproj index 39bf131f8..e326e5ed3 100644 --- a/src/Prowlarr.Http/Prowlarr.Http.csproj +++ b/src/Prowlarr.Http/Prowlarr.Http.csproj @@ -4,8 +4,8 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="ImpromptuInterface" Version="7.0.1" /> - <PackageReference Include="NLog" Version="5.2.0" /> + <PackageReference Include="ImpromptuInterface" Version="7.0.1" /> + <PackageReference Include="NLog" Version="5.4.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" /> diff --git a/yarn.lock b/yarn.lock index e94d1f4c2..c63e95086 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,130 +2,158 @@ # yarn lockfile v1 -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - "@adobe/css-tools@^4.0.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" - integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== "@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" + integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" + "@babel/highlight" "^7.25.7" + picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" - integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/core@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.11.tgz#8033acaa2aa24c3f814edaaa057f3ce0ba559c24" - integrity sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" + integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== + +"@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== + +"@babel/core@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helpers" "^7.22.11" - "@babel/parser" "^7.22.11" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.11" - "@babel/types" "^7.22.11" - convert-source-map "^1.7.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/eslint-parser@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.22.11.tgz#cceb8c7989c241a16dd14e12a6cd725618f3f58b" - integrity sha512-YjOYZ3j7TjV8OhLW6NCtyg8G04uStATEUe5eiLuCZaXz2VSDQ3dsAtm2D+TuQyAqNMUK2WacGo0/uma9Pein1w== +"@babel/eslint-parser@7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz#603c68a63078796527bc9d0833f5e52dd5f9224c" + integrity sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/types" "^7.22.10" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== +"@babel/helper-annotate-as-pure@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" + integrity sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.25.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz#573e735937e99ea75ea30788b57eb52fab7468c9" - integrity sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ== +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.25.9" -"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== +"@babel/helper-compilation-targets@^7.22.6": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" + integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" - browserslist "^4.21.9" + "@babel/compat-data" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz#4078686740459eeb4af3494a273ac09148dfb213" - integrity sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ== +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" - integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - regexpu-core "^5.3.1" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-create-regexp-features-plugin@^7.18.6": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" + integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.7" + regexpu-core "^6.1.1" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.2.0" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -133,317 +161,223 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" - integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== - -"@babel/helper-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-member-expression-to-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" - integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== dependencies: - "@babel/types" "^7.22.5" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.25.9" -"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" - integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" + integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== + +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-optimise-call-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" - integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - -"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" - integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-wrap-function" "^7.22.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" - integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== +"@babel/helper-string-parser@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" + integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" + integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" + integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== dependencies: - "@babel/types" "^7.22.5" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" -"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" - integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== +"@babel/highlight@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" + integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== - -"@babel/helper-validator-option@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" - integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== - -"@babel/helper-wrap-function@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz#d845e043880ed0b8c18bd194a12005cb16d2f614" - integrity sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ== - dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.10" - -"@babel/helpers@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a" - integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg== - dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.11" - "@babel/types" "^7.22.11" - -"@babel/highlight@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" - integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== - dependencies: - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.25.7" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/parser@^7.22.11", "@babel/parser@^7.22.5": - version "7.22.14" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.14.tgz#c7de58e8de106e88efca42ce17f0033209dfd245" - integrity sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" - integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/types" "^7.26.3" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" - integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-proposal-export-default-from@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.22.5.tgz#825924eda1fad382c3de4db6fe1711b6fa03362f" - integrity sha512-UCe1X/hplyv6A5g2WnQ90tnHRvYL29dabCWww92lO7VdfMVTVReBTRrhiMrKQejHD9oVkdnRdwYuzUZkBVQisg== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-export-default-from" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-proposal-export-default-from@7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.9.tgz#52702be6ef8367fc8f18b8438278332beeb8f87c" + integrity sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@7.8.3", "@babel/plugin-syntax-dynamic-import@^7.8.3": +"@babel/plugin-syntax-dynamic-import@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-default-from@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz#ac3a24b362a04415a017ab96b9b4483d0e2a6e44" - integrity sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ== +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-assertions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" - integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== +"@babel/plugin-syntax-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-attributes@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" - integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" - integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" - integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -453,521 +387,510 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" - integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-async-generator-functions@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz#dbe3b1ff5a52e2e5edc4b19a60d325a675ed2649" - integrity sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw== +"@babel/plugin-transform-async-generator-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-async-to-generator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" - integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== dependencies: - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" -"@babel/plugin-transform-block-scoped-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" - integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== +"@babel/plugin-transform-block-scoped-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz#5700691dbd7abb93de300ca7be94203764fce458" + integrity sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-block-scoping@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" - integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-class-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" - integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-class-static-block@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" - integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-classes@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" - integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" - integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" -"@babel/plugin-transform-destructuring@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" - integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-dotall-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" - integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-keys@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" - integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-dynamic-import@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" - integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-exponentiation-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" - integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-export-namespace-from@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" - integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== +"@babel/plugin-transform-exponentiation-operator@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-for-of@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" - integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" - integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz#4bdc7d42a213397905d89f02350c5267866d5755" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== dependencies: - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-json-strings@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" - integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" - integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-logical-assignment-operators@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" - integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-member-expression-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" - integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-amd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" - integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-commonjs@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz#d7991d3abad199c03b68ee66a64f216c47ffdfae" - integrity sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g== +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== dependencies: - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-systemjs@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1" - integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA== +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-umd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" - integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" - integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-new-target@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" - integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" - integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-numeric-separator@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" - integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz#bcb1b0d9e948168102d5f7104375ca21c3266949" + integrity sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-object-rest-spread@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz#dbbb06ce783cd994a8f430d8cefa553e9b42ca62" - integrity sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw== +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-object-super@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" - integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" -"@babel/plugin-transform-optional-catch-binding@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" - integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" -"@babel/plugin-transform-optional-chaining@^7.22.12", "@babel/plugin-transform-optional-chaining@^7.22.5": - version "7.22.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz#d7ebf6a88cd2f4d307b0e000ab630acd8124b333" - integrity sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw== +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-parameters@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" - integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-private-methods@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" - integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-private-property-in-object@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" - integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-property-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" - integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-display-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" - integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-jsx-development@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" - integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== +"@babel/plugin-transform-react-display-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" + integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== dependencies: - "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" - integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== +"@babel/plugin-transform-react-jsx-development@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" + integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/plugin-transform-react-jsx" "^7.25.9" -"@babel/plugin-transform-react-pure-annotations@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz#1f58363eef6626d6fa517b95ac66fe94685e32c0" - integrity sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA== +"@babel/plugin-transform-react-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" + integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/plugin-transform-regenerator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" - integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== +"@babel/plugin-transform-react-pure-annotations@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" + integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" - integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-shorthand-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" - integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" - integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-sticky-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" - integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-template-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" - integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-typeof-symbol@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" - integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== +"@babel/plugin-transform-template-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz#6dbd4a24e8fad024df76d1fac6a03cf413f60fe1" + integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-typescript@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz#9f27fb5e51585729374bb767ab6a6d9005a23329" - integrity sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA== +"@babel/plugin-transform-typeof-symbol@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz#224ba48a92869ddbf81f9b4a5f1204bbf5a2bc4b" + integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.11" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-typescript" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-escapes@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" - integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== +"@babel/plugin-transform-typescript@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz#3d6add9c78735623317387ee26d5ada540eee3fd" + integrity sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" -"@babel/plugin-transform-unicode-property-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" - integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" - integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-unicode-sets-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" - integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/preset-env@7.22.14": - version "7.22.14" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.14.tgz#1cbb468d899f64fa71c53446f13b7ff8c0005cc1" - integrity sha512-daodMIoVo+ol/g+//c/AH+szBkFj4STQUikvBijRGL72Ph+w+AMTSh55DUETe8KJlPlDT1k/mp7NBfOuiWmoig== +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.0.tgz#30e5c6bc1bcc54865bff0c5a30f6d4ccdc7fa8b1" + integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== + dependencies: + "@babel/compat-data" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.22.5" - "@babel/plugin-syntax-import-attributes" "^7.22.5" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.11" - "@babel/plugin-transform-async-to-generator" "^7.22.5" - "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.10" - "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.11" - "@babel/plugin-transform-classes" "^7.22.6" - "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.10" - "@babel/plugin-transform-dotall-regex" "^7.22.5" - "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.11" - "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.11" - "@babel/plugin-transform-for-of" "^7.22.5" - "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.11" - "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" - "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.11" - "@babel/plugin-transform-modules-systemjs" "^7.22.11" - "@babel/plugin-transform-modules-umd" "^7.22.5" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" - "@babel/plugin-transform-numeric-separator" "^7.22.11" - "@babel/plugin-transform-object-rest-spread" "^7.22.11" - "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.11" - "@babel/plugin-transform-optional-chaining" "^7.22.12" - "@babel/plugin-transform-parameters" "^7.22.5" - "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.11" - "@babel/plugin-transform-property-literals" "^7.22.5" - "@babel/plugin-transform-regenerator" "^7.22.10" - "@babel/plugin-transform-reserved-words" "^7.22.5" - "@babel/plugin-transform-shorthand-properties" "^7.22.5" - "@babel/plugin-transform-spread" "^7.22.5" - "@babel/plugin-transform-sticky-regex" "^7.22.5" - "@babel/plugin-transform-template-literals" "^7.22.5" - "@babel/plugin-transform-typeof-symbol" "^7.22.5" - "@babel/plugin-transform-unicode-escapes" "^7.22.10" - "@babel/plugin-transform-unicode-property-regex" "^7.22.5" - "@babel/plugin-transform-unicode-regex" "^7.22.5" - "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.25.9" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.25.9" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.25.9" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.25.9" + "@babel/plugin-transform-typeof-symbol" "^7.25.9" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.11" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" - core-js-compat "^3.31.0" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.38.1" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -979,89 +902,89 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6" - integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ== +"@babel/preset-react@7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-transform-react-display-name" "^7.22.5" - "@babel/plugin-transform-react-jsx" "^7.22.5" - "@babel/plugin-transform-react-jsx-development" "^7.22.5" - "@babel/plugin-transform-react-pure-annotations" "^7.22.5" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-transform-react-display-name" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx-development" "^7.25.9" + "@babel/plugin-transform-react-pure-annotations" "^7.25.9" -"@babel/preset-typescript@7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz#f218cd0345524ac888aa3dc32f029de5b064b575" - integrity sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg== +"@babel/preset-typescript@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz#4a570f1b8d104a242d923957ffa1eaff142a106d" + integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.11" - "@babel/plugin-transform-typescript" "^7.22.11" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-typescript" "^7.25.9" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4" - integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/traverse@^7.22.11": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.11.tgz#71ebb3af7a05ff97280b83f05f8865ac94b2027c" - integrity sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ== +"@babel/traverse@^7.25.9": + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.11" - "@babel/types" "^7.22.11" - debug "^4.1.0" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5", "@babel/types@^7.4.4": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.11.tgz#0e65a6a1d4d9cbaa892b2213f6159485fe632ea2" - integrity sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg== +"@babel/types@^7.25.7", "@babel/types@^7.4.4": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" + integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-string-parser" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@csstools/css-parser-algorithms@^2.1.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" - integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz#6d93a8f7d8aeb7cd9ed0868f946e46f021b6aa70" + integrity sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw== "@csstools/css-tokenizer@^2.1.1": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" - integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== + version "2.4.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz#1d8b2e200197cf5f35ceb07ca2dade31f3a00ae8" + integrity sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg== "@csstools/media-query-list-parser@^2.0.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz#0017f99945f6c16dd81a7aacf6821770933c3a5c" - integrity sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw== + version "2.1.13" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz#f00be93f6bede07c14ddf51a168ad2748e4fe9e5" + integrity sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA== "@csstools/selector-specificity@^2.2.0": version "2.2.0" @@ -1073,22 +996,27 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005" - integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg== +"@eslint-community/regexpp@^4.10.0": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/eslintrc@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint-community/regexpp@^4.6.1": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1100,56 +1028,56 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.44.0": - version "8.44.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" - integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@fortawesome/fontawesome-common-types@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" - integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ== +"@fortawesome/fontawesome-common-types@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz#6201640f39fdcf8e41cd9d1a92b2da3a96817fa4" + integrity sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ== -"@fortawesome/fontawesome-free@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe" - integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ== +"@fortawesome/fontawesome-free@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz#160a48730d533ec77578ed0141661a8f0150a71d" + integrity sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA== -"@fortawesome/fontawesome-svg-core@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz#3727552eff9179506e9203d72feb5b1063c11a21" - integrity sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw== +"@fortawesome/fontawesome-svg-core@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz#1f8ebb6f35cf02f89c110198514e848de17ac99e" + integrity sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.7.1" -"@fortawesome/free-regular-svg-icons@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz#cacc53bd8d832d46feead412d9ea9ce80a55e13a" - integrity sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw== +"@fortawesome/free-regular-svg-icons@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.1.tgz#d7ec06f896ee91116a388a5a234cd26420ccdfe4" + integrity sha512-e13cp+bAx716RZOTQ59DhqikAgETA9u1qTBHO3e3jMQQ+4H/N1NC1ZVeFYt1V0m+Th68BrEL1/X6XplISutbXg== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.7.1" -"@fortawesome/free-solid-svg-icons@6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz#48c0e790847fa56299e2f26b82b39663b8ad7119" - integrity sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ== +"@fortawesome/free-solid-svg-icons@6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz#c1f9a6c25562a12c283e87e284f9d82a5b0dbcc0" + integrity sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.0" + "@fortawesome/fontawesome-common-types" "6.7.1" -"@fortawesome/react-fontawesome@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" - integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== +"@fortawesome/react-fontawesome@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4" + integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g== dependencies: prop-types "^15.8.1" -"@humanwhocodes/config-array@^0.11.10": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" - integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -1157,47 +1085,59 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: - "@jridgewell/set-array" "^1.0.1" + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1266,96 +1206,104 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@sentry-internal/tracing@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.51.2.tgz#17833047646426ca71445327018ffcb33506a699" - integrity sha512-OBNZn7C4CyocmlSMUPfkY9ORgab346vTHu5kX35PgW5XR51VD2nO5iJCFbyFcsmmRWyCJcZzwMNARouc2V4V8A== - dependencies: - "@sentry/core" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry/browser@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.51.2.tgz#c01758a54c613be45df58ab503805737256f51a4" - integrity sha512-FQFEaTFbvYHPQE2emFjNoGSy+jXplwzoM/XEUBRjrGo62lf8BhMvWnPeG3H3UWPgrWA1mq0amvHRwXUkwofk0g== +"@sentry-internal/feedback@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.119.1.tgz#98285dc9dba0ab62369d758124901b00faf58697" + integrity sha512-EPyW6EKZmhKpw/OQUPRkTynXecZdYl4uhZwdZuGqnGMAzswPOgQvFrkwsOuPYvoMfXqCH7YuRqyJrox3uBOrTA== dependencies: - "@sentry-internal/tracing" "7.51.2" - "@sentry/core" "7.51.2" - "@sentry/replay" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/core@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.51.2.tgz#f2c938de334f9bf26f4416079168275832423964" - integrity sha512-p8ZiSBxpKe+rkXDMEcgmdoyIHM/1bhpINLZUFPiFH8vzomEr7sgnwRhyrU8y/ADnkPeNg/2YF3QpDpk0OgZJUA== +"@sentry-internal/replay-canvas@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.119.1.tgz#b1413fb37734d609b0745ac24d49ddf9d63b9c51" + integrity sha512-O/lrzENbMhP/UDr7LwmfOWTjD9PLNmdaCF408Wx8SDuj7Iwc+VasGfHg7fPH4Pdr4nJON6oh+UqoV4IoG05u+A== dependencies: - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" - tslib "^1.9.3" + "@sentry/core" "7.119.1" + "@sentry/replay" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/integrations@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.51.2.tgz#fce58b9ced601c7f93344b508c67c69a9c883f3d" - integrity sha512-ZnSptbuDQOoQ13mFX9vvLDfXlbMGjenW2fMIssi9+08B7fD6qxmetkYnWmBK+oEipjoGA//0240Fj8FUvZr0Qg== +"@sentry-internal/tracing@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.119.1.tgz#500d50d451bfd0ce6b185e9f112208229739ab03" + integrity sha512-cI0YraPd6qBwvUA3wQdPGTy8PzAoK0NZiaTN1LM3IczdPegehWOaEG5GVTnpGnTsmBAzn1xnBXNBhgiU4dgcrQ== dependencies: - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" + +"@sentry/browser@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.119.1.tgz#260470dd7fd18de366017c3bf23a252a24d2ff05" + integrity sha512-aMwAnFU4iAPeLyZvqmOQaEDHt/Dkf8rpgYeJ0OEi50dmP6AjG+KIAMCXU7CYCCQDn70ITJo8QD5+KzCoZPYz0A== + dependencies: + "@sentry-internal/feedback" "7.119.1" + "@sentry-internal/replay-canvas" "7.119.1" + "@sentry-internal/tracing" "7.119.1" + "@sentry/core" "7.119.1" + "@sentry/integrations" "7.119.1" + "@sentry/replay" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" + +"@sentry/core@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.119.1.tgz#63e949cad167a0ee5e52986c93b96ff1d6a05b57" + integrity sha512-YUNnH7O7paVd+UmpArWCPH4Phlb5LwrkWVqzFWqL3xPyCcTSof2RL8UmvpkTjgYJjJ+NDfq5mPFkqv3aOEn5Sw== + dependencies: + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" + +"@sentry/integrations@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.119.1.tgz#9fc17aa9fcb942fbd2fc12eecd77a0f316897960" + integrity sha512-CGmLEPnaBqbUleVqrmGYjRjf5/OwjUXo57I9t0KKWViq81mWnYhaUhRZWFNoCNQHns+3+GPCOMvl0zlawt+evw== + dependencies: + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" localforage "^1.8.1" - tslib "^1.9.3" -"@sentry/replay@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.51.2.tgz#1f54e92b472ab87dfdb4e8cd6b8c8252600fe7b0" - integrity sha512-W8YnSxkK9LTUXDaYciM7Hn87u57AX9qvH8jGcxZZnvpKqHlDXOpSV8LRtBkARsTwgLgswROImSifY0ic0lyCWg== +"@sentry/replay@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.119.1.tgz#117cf493a3008a39943b7d571d451c6218542847" + integrity sha512-4da+ruMEipuAZf35Ybt2StBdV1S+oJbSVccGpnl9w6RoeQoloT4ztR6ML3UcFDTXeTPT1FnHWDCyOfST0O7XMw== dependencies: - "@sentry/core" "7.51.2" - "@sentry/types" "7.51.2" - "@sentry/utils" "7.51.2" + "@sentry-internal/tracing" "7.119.1" + "@sentry/core" "7.119.1" + "@sentry/types" "7.119.1" + "@sentry/utils" "7.119.1" -"@sentry/types@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.51.2.tgz#cb742f374d9549195f62c462c915adeafed31d65" - integrity sha512-/hLnZVrcK7G5BQoD/60u9Qak8c9AvwV8za8TtYPJDUeW59GrqnqOkFji7RVhI7oH1OX4iBxV+9pAKzfYE6A6SA== +"@sentry/types@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.119.1.tgz#f9c3c12e217c9078a6d556c92590e42a39b750dd" + integrity sha512-4G2mcZNnYzK3pa2PuTq+M2GcwBRY/yy1rF+HfZU+LAPZr98nzq2X3+mJHNJoobeHRkvVh7YZMPi4ogXiIS5VNQ== -"@sentry/utils@7.51.2": - version "7.51.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.51.2.tgz#2a52ac2cfb00ffd128248981279c0a561b39eccb" - integrity sha512-EcjBU7qG4IG+DpIPvdgIBcdIofROMawKoRUNKraeKzH/waEYH9DzCaqp/mzc5/rPBhpDB4BShX9xDDSeH+8c0A== +"@sentry/utils@7.119.1": + version "7.119.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.119.1.tgz#08b28fa8170987a60e149e2102e83395a95e9a89" + integrity sha512-ju/Cvyeu/vkfC5/XBV30UNet5kLEicZmXSyuLwZu95hEbL+foPdxN+re7pCI/eNqfe3B2vz7lvz5afLVOlQ2Hg== dependencies: - "@sentry/types" "7.51.2" - tslib "^1.9.3" + "@sentry/types" "7.119.1" "@types/archiver@^5.3.1": - version "5.3.2" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.2.tgz#a9f0bcb0f0b991400e7766d35f6e19d163bdadcc" - integrity sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw== + version "5.3.4" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" + integrity sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw== dependencies: "@types/readdir-glob" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.44.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a" - integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/history@^4.7.11": version "4.7.11" @@ -1363,9 +1311,9 @@ integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== "@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" @@ -1375,76 +1323,87 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" - integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@4.14.194": - version "4.14.194" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" - integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== +"@types/lodash@4.14.195": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== "@types/minimist@^1.2.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@*": - version "20.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" - integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" -"@types/node@18.15.11": - version "18.15.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" - integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== +"@types/node@20.16.11": + version "20.16.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.11.tgz#9b544c3e716b1577ac12e70f9145193f32750b33" + integrity sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw== + dependencies: + undici-types "~6.19.2" "@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/postcss-modules-local-by-default@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#5c141c9bd3a994ae1ebe23d2ae094b24d19538f5" - integrity sha512-0VLab/pcLTLcfbxi6THSIMVYcw9hEUBGvjwwaGpW77mMgRXfGF+a76t7BxTGyLh1y68tBvrffp8UWnqvm76+yg== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz#8fee7513dd1558d74713d817c183a33a6dc583f9" + integrity sha512-CtYCcD+L+trB3reJPny+bKWKMzPfxEyQpKIwit7kErnOexf5/faaGpkFy4I5AwbV4hp1sk7/aTg0tt0B67VkLQ== dependencies: postcss "^8.0.0" "@types/postcss-modules-scope@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/postcss-modules-scope/-/postcss-modules-scope-3.0.1.tgz#f0ad443c2f31f90feacb83bb357692d581388afd" - integrity sha512-LNkp3c4ML9EQj2dgslp4i80Jxj72YK3HjYzrTn6ftUVylW1zaKFGqrMlNIyqBmPWmIhZ/Y5r0Y4T49Hk1IuDUg== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/postcss-modules-scope/-/postcss-modules-scope-3.0.4.tgz#f82d15ec9023c924b531a49e8087b32646233f41" + integrity sha512-//ygSisVq9kVI0sqx3UPLzWIMCmtSVrzdljtuaAEJtGoGnpjBikZ2sXO5MpH9SnWX9HRfXxHifDAXcQjupWnIQ== dependencies: postcss "^8.0.0" "@types/prop-types@*": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== -"@types/react-dom@18.2.4": - version "18.2.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.4.tgz#13f25bfbf4e404d26f62ac6e406591451acba9e0" - integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw== +"@types/react-document-title@2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/react-document-title/-/react-document-title-2.0.10.tgz#f9c4563744b735750d84519ba1bc7099e1b2d1d0" + integrity sha512-a5RYXFccVqVhc429yXUn9zjJvaQwdx3Kueb8v8pEymUyExHoatHv0iS8BlOE3YuS+csA2pHbL2Hatnp7QEtLxQ== + dependencies: + "@types/react" "*" + +"@types/react-dom@18.2.25": + version "18.2.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" + integrity sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA== dependencies: "@types/react" "*" "@types/react-redux@^7.1.16": - version "7.1.26" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.26.tgz#84149f5614e40274bb70fcbe8f7cae6267d548b1" - integrity sha512-UKPo7Cm7rswYU6PH6CmTNCRv5NYF3HrgKuHEYTK8g/3czYLrUux50gQ2pkxc9c7ZpQZi+PNhgmI8oNIRoiVIxg== + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1468,92 +1427,80 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react-text-truncate@0.14.1": - version "0.14.1" - resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28" - integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw== +"@types/react-text-truncate@0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.19.0.tgz#322dd718dcbe1267a9d1279f8ac4dc844c322a13" + integrity sha512-8H7BjVf7Rp3ERTTiFZpQf6a5hllwdJrWuQ92nwQGp7DWQ2Ju89GRuzXHuZHXU9T+hLTGLCUPbimjQnW1mAogqQ== dependencies: "@types/react" "*" -"@types/react-window@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" - integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== +"@types/react-window@1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== dependencies: "@types/react" "*" "@types/react@*": - version "18.2.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" - integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== + version "18.3.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.11.tgz#9d530601ff843ee0d7030d4227ea4360236bd537" + integrity sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ== dependencies: "@types/prop-types" "*" - "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.2.6": - version "18.2.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.6.tgz#5cd53ee0d30ffc193b159d3516c8c8ad2f19d571" - integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA== +"@types/react@18.2.79": + version "18.2.79" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" + integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== dependencies: "@types/prop-types" "*" - "@types/scheduler" "*" csstype "^3.0.2" "@types/readdir-glob@*": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" - integrity sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ== + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" + integrity sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg== dependencies: "@types/node" "*" -"@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== - -"@types/semver@^7.3.12": - version "7.5.1" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367" - integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg== - "@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.6.tgz#164e169dd061795b50b83c19e4d3be09f8d3a454" + integrity sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g== "@types/tapable@^1": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" - integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== + version "1.0.12" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.12.tgz#bc2cab12e87978eee89fb21576b670350d6d86ab" + integrity sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q== "@types/uglify-js@*": - version "3.17.2" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.2.tgz#a2ba86fd524f6281a7655463338c546f845b29c3" - integrity sha512-9SjrHO54LINgC/6Ehr81NjAxAYvwEZqjUHLjJYvC4Nmr9jbLQCIZbWSvl4vXQkkmR1UAuaKDycau3O1kWGFyXQ== + version "3.17.5" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.5.tgz#905ce03a3cbbf2e31cbefcbc68d15497ee2e17df" + integrity sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ== dependencies: source-map "^0.6.1" -"@types/webpack-livereload-plugin@2.3.3": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.3.tgz#96f34133f1e1515571233a3e5099d863dc8723e7" - integrity sha512-R8P2HG2mAHY3Qptspt0il8zYVNqiEeOmMe2cGFcEjH7qnJZ4uC7hujLwfAm6jrIO7I5uEs6CxfpKSXM9ULAggw== +"@types/webpack-livereload-plugin@2.3.6": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.6.tgz#2c3ccefc8858525f40aeb8be0f784d5027144e23" + integrity sha512-H8nZSOWSiY/6kCpOmbutZPu7Sai1xyEXo/SrXQPCymMPNBwpYWAdOsjKqr32d+IrVjnn9GGgKSYY34TEPRxJ/A== dependencies: "@types/webpack" "^4" "@types/webpack-sources@*": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" - integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg== + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.3.tgz#b667bd13e9fa15a9c26603dce502c7985418c3d8" + integrity sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw== dependencies: "@types/node" "*" "@types/source-list-map" "*" source-map "^0.7.3" "@types/webpack@^4": - version "4.41.33" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc" - integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g== + version "4.41.39" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.39.tgz#ab6feaeef8e074d0b584bbe4a4e2dc604b58eed7" + integrity sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg== dependencies: "@types/node" "*" "@types/tapable" "^1" @@ -1562,94 +1509,96 @@ anymatch "^3.0.0" source-map "^0.6.0" -"@typescript-eslint/eslint-plugin@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz#f156827610a3f8cefc56baeaa93cd4a5f32966b4" - integrity sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg== +"@typescript-eslint/eslint-plugin@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz#992e5ac1553ce20d0d46aa6eccd79dc36dedc805" + integrity sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ== dependencies: - "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/type-utils" "5.59.5" - "@typescript-eslint/utils" "5.59.5" - debug "^4.3.4" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - semver "^7.3.7" - tsutils "^3.21.0" + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/type-utils" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/parser@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.5.tgz#63064f5eafbdbfb5f9dfbf5c4503cdf949852981" - integrity sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw== +"@typescript-eslint/parser@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.1.tgz#c258bae062778b7696793bc492249027a39dfb95" + integrity sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA== dependencies: - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz#33ffc7e8663f42cfaac873de65ebf65d2bce674d" - integrity sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A== +"@typescript-eslint/scope-manager@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz#52cedc3a8178d7464a70beffed3203678648e55b" + integrity sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" -"@typescript-eslint/type-utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz#485b0e2c5b923460bc2ea6b338c595343f06fc9b" - integrity sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg== +"@typescript-eslint/type-utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz#10f41285475c0bdee452b79ff7223f0e43a7781e" + integrity sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.5" - "@typescript-eslint/utils" "5.59.5" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils" "8.18.1" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/types@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.5.tgz#e63c5952532306d97c6ea432cee0981f6d2258c7" - integrity sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w== +"@typescript-eslint/types@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.1.tgz#d7f4f94d0bba9ebd088de840266fcd45408a8fff" + integrity sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw== -"@typescript-eslint/typescript-estree@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz#9b252ce55dd765e972a7a2f99233c439c5101e42" - integrity sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg== +"@typescript-eslint/typescript-estree@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz#2a86cd64b211a742f78dfa7e6f4860413475367e" + integrity sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg== dependencies: - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/visitor-keys" "5.59.5" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/utils@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.5.tgz#15b3eb619bb223302e60413adb0accd29c32bcae" - integrity sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA== +"@typescript-eslint/utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.1.tgz#c4199ea23fc823c736e2c96fd07b1f7235fa92d5" + integrity sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.5" - "@typescript-eslint/types" "5.59.5" - "@typescript-eslint/typescript-estree" "5.59.5" - eslint-scope "^5.1.1" - semver "^7.3.7" + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" -"@typescript-eslint/visitor-keys@5.59.5": - version "5.59.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz#ba5b8d6791a13cf9fea6716af1e7626434b29b9b" - integrity sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA== +"@typescript-eslint/visitor-keys@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz#344b4f6bc83f104f514676facf3129260df7610a" + integrity sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ== dependencies: - "@typescript-eslint/types" "5.59.5" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "8.18.1" + eslint-visitor-keys "^4.2.0" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -1664,10 +1613,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -1683,15 +1632,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -1712,59 +1661,59 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.1": @@ -1799,10 +1748,10 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -1815,9 +1764,9 @@ acorn@^6.0.6: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== add-px-to-style@1.0.0: version "1.0.0" @@ -1851,7 +1800,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1862,50 +1811,24 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-cyan@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" - integrity sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A== - dependencies: - ansi-wrap "0.1.0" - -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw== - dependencies: - ansi-wrap "0.1.0" - -ansi-red@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" - integrity sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow== - dependencies: - ansi-wrap "0.1.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^3.2.1: version "3.2.1" @@ -1921,10 +1844,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-wrap@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== anymatch@^3.0.0, anymatch@^3.1.1, anymatch@~3.1.2: version "3.1.3" @@ -1994,99 +1917,103 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -arr-diff@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" - integrity sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q== +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - arr-flatten "^1.0.1" - array-slice "^0.2.3" - -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" - integrity sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA== - -array-buffer-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" - integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== - dependencies: - call-bind "^1.0.2" - is-array-buffer "^3.0.1" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" array-flatten@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-includes@^3.1.6: - version "3.1.7" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" - integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" is-string "^1.0.7" -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q== - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== +array.prototype.findlastindex@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.tosorted@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" - -arraybuffer.prototype.slice@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" - integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== - dependencies: - array-buffer-byte-length "^1.0.0" call-bind "^1.0.2" define-properties "^1.2.0" - get-intrinsic "^1.2.1" - is-array-buffer "^3.0.2" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" arrify@^1.0.1: @@ -2107,31 +2034,33 @@ async@^2.6.4: lodash "^4.17.14" async@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -autoprefixer@10.4.14: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== +autoprefixer@10.4.20: + version "10.4.20" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.0" + picocolors "^1.0.1" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" -babel-loader@9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== +babel-loader@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== dependencies: find-cache-dir "^4.0.0" schema-utils "^4.0.0" @@ -2141,29 +2070,29 @@ babel-plugin-inline-classnames@2.0.1: resolved "https://registry.yarnpkg.com/babel-plugin-inline-classnames/-/babel-plugin-inline-classnames-2.0.1.tgz#d871490af06781a42f231a1e090bc4133594f168" integrity sha512-Pq/jJ6hTiGiqcMmy2d4CyJcfBDeUHOdQl1t1MDWNaSKR2RxDmShSAx4Zqz6NDmFaiinaRqF8eQoTVgSRGU+McQ== -babel-plugin-polyfill-corejs2@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" - integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.31.0" + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" -babel-plugin-polyfill-regenerator@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.6.2" babel-plugin-transform-react-remove-prop-types@0.4.24: version "0.4.24" @@ -2204,9 +2133,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bl@^4.0.3: version "4.1.0" @@ -2247,22 +2176,22 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" -browserslist@^4.14.5, browserslist@^4.21.10, browserslist@^4.21.5, browserslist@^4.21.9: - version "4.21.10" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" - integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== +browserslist@^4.21.10, browserslist@^4.23.3, browserslist@^4.24.0: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== dependencies: - caniuse-lite "^1.0.30001517" - electron-to-chromium "^1.4.477" - node-releases "^2.0.13" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" @@ -2287,13 +2216,16 @@ bytes@1: resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ== -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" @@ -2327,21 +2259,10 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517: - version "1.0.30001525" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz#d2e8fdec6116ffa36284ca2c33ef6d53612fe1c8" - integrity sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q== - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" +caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: + version "1.0.30001667" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz#99fc5ea0d9c6e96897a104a8352604378377f949" + integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -2360,17 +2281,17 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chart.js@4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1" - integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g== +chart.js@4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.4.tgz#b682d2e7249f7a0cbb1b1d31c840266ae9db64b7" + integrity sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA== dependencies: "@kurkle/color" "^0.3.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -2382,20 +2303,27 @@ chart.js@4.3.0: optionalDependencies: fsevents "~2.3.2" -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== +chokidar@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" -classnames@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +classnames@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-css@^5.2.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" - integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== dependencies: source-map "~0.6.0" @@ -2404,15 +2332,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2463,11 +2382,6 @@ color-string@^0.3.0: dependencies: color-name "^1.0.0" -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - color@^0.11.0: version "0.11.4" resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" @@ -2538,10 +2452,10 @@ continuable-cache@^0.3.1: resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" integrity sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA== -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== copy-anything@^2.0.1: version "2.0.6" @@ -2550,17 +2464,24 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -core-js-compat@^3.31.0: - version "3.32.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.1.tgz#55f9a7d297c0761a8eb1d31b593e0f5b6ffae964" - integrity sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA== +copy-to-clipboard@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== dependencies: - browserslist "^4.21.10" + toggle-selection "^1.0.6" -core-js@3.33.0: - version "3.33.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40" - integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw== +core-js-compat@^3.38.0, core-js-compat@^3.38.1: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" + integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== + dependencies: + browserslist "^4.23.3" + +core-js@3.39.0: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-js@^2.4.0: version "2.6.12" @@ -2584,9 +2505,9 @@ cosmiconfig@^7.0.1: yaml "^1.10.0" cosmiconfig@^8.1.3: - version "8.3.3" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.3.tgz#45985f9f39f3c9330288ef642b1dcb7342bd76d7" - integrity sha512-/VY+0IvFoE47hwgKHu8feeBFIb1Z1mcJFiLrNwaJpLoLa9qwLVquMGMr2OUwQmhpJDtsSQSasg/TMv1imec9xA== + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: import-fresh "^3.3.0" js-yaml "^4.1.0" @@ -2614,7 +2535,7 @@ create-react-context@^0.3.0: gud "^1.0.0" warning "^4.0.3" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2634,9 +2555,9 @@ css-color-function@~1.3.3: rgb "~0.1.0" css-functions-list@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.0.tgz#8290b7d064bf483f48d6559c10e98dc4d1ad19ee" - integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg== + version "3.2.3" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" + integrity sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA== css-loader@6.7.3: version "6.7.3" @@ -2690,33 +2611,60 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" decamelize-keys@^1.1.0: version "1.1.1" @@ -2732,16 +2680,16 @@ decamelize@^1.1.0, decamelize@^1.2.0: integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== deep-equal@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + version "1.1.2" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.2.tgz#78a561b7830eef3134c7f6f3a3d6af272a678761" + integrity sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg== dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" + is-arguments "^1.1.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + object-is "^1.1.5" object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" + regexp.prototype.flags "^1.5.1" deep-is@^0.1.3: version "0.1.4" @@ -2753,11 +2701,21 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -2775,11 +2733,6 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -2882,14 +2835,19 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.0.3: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== -electron-to-chromium@^1.4.477: - version "1.4.508" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz#5641ff2f5ba11df4bd960fe6a2f9f70aa8b9af96" - integrity sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.5.28: + version "1.5.35" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.35.tgz#1d38d386186c72b1fa6e74c3a7de5f888b503100" + integrity sha512-hOSRInrIDm0Brzp4IHW2F/VM+638qOL2CzE0DgpnGzKW27C95IqqeqgKz/hxHGnvPxvQGpHUGD5qRVC9EZY2+A== element-class@0.2.2: version "0.2.2" @@ -2901,6 +2859,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -2913,10 +2876,10 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2927,9 +2890,9 @@ entities@^2.0.0: integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== envinfo@^7.7.3: - version "7.10.0" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" - integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== errno@^0.1.1: version "0.1.8" @@ -2959,71 +2922,117 @@ error@^7.0.0: dependencies: string-template "~0.2.1" -es-abstract@^1.20.4, es-abstract@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" - integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: - array-buffer-byte-length "^1.0.0" - arraybuffer.prototype.slice "^1.0.1" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.1" - get-symbol-description "^1.0.0" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" globalthis "^1.0.3" gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" is-callable "^1.2.7" - is-negative-zero "^2.0.2" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" + is-shared-array-buffer "^1.0.3" is-string "^1.0.7" - is-typed-array "^1.1.10" + is-typed-array "^1.1.13" is-weakref "^1.0.2" - object-inspect "^1.12.3" + object-inspect "^1.13.1" object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.0" - safe-array-concat "^1.0.0" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-buffer "^1.0.0" - typed-array-byte-length "^1.0.0" - typed-array-byte-offset "^1.0.0" - typed-array-length "^1.0.4" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" unbox-primitive "^1.0.2" - which-typed-array "^1.1.10" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.19: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" + integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.4" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.3" + safe-array-concat "^1.1.2" es-module-lexer@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" - integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== - dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" - -es-shim-unscopables@^1.0.0: +es-object-atoms@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== dependencies: - has "^1.0.3" + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -3039,12 +3048,12 @@ es6-promise@^4.2.8: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== @@ -3054,12 +3063,12 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" - integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== +eslint-config-prettier@8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== -eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -3068,10 +3077,10 @@ eslint-import-resolver-node@^0.3.7: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-module-utils@^2.7.4: - version "2.8.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" - integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" @@ -3085,26 +3094,30 @@ eslint-plugin-filenames@1.3.2: lodash.snakecase "4.1.1" lodash.upperfirst "4.3.1" -eslint-plugin-import@2.27.5: - version "2.27.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== +eslint-plugin-import@2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" - eslint-module-utils "^2.7.4" - has "^1.0.3" - is-core-module "^2.11.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.0" + hasown "^2.0.2" + is-core-module "^2.15.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.6" - resolve "^1.22.1" - semver "^6.3.0" - tsconfig-paths "^3.14.1" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" + semver "^6.3.1" + string.prototype.trimend "^1.0.8" + tsconfig-paths "^3.15.0" eslint-plugin-prettier@4.2.1: version "4.2.1" @@ -3113,38 +3126,41 @@ eslint-plugin-prettier@4.2.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== +eslint-plugin-react-hooks@4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@7.32.2: - version "7.32.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" - integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== +eslint-plugin-react@7.37.1: + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" prop-types "^15.8.1" - resolve "^2.0.0-next.4" - semver "^6.3.0" - string.prototype.matchall "^4.0.8" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" -eslint-plugin-simple-import-sort@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz#cc4ceaa81ba73252427062705b64321946f61351" - integrity sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw== +eslint-plugin-simple-import-sort@12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz#e64bfdaf91c5b98a298619aa634a9f7aa43b709e" + integrity sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA== -eslint-scope@5.1.1, eslint-scope@^5.1.1: +eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -3152,7 +3168,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.0: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -3165,32 +3181,38 @@ eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.45.0: - version "8.45.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78" - integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@8.57.1: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.1.0" - "@eslint/js" "8.44.0" - "@humanwhocodes/config-array" "^0.11.10" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.1" - espree "^9.6.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3213,7 +3235,7 @@ eslint@8.45.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.6.0: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -3223,9 +3245,9 @@ espree@^9.6.0: eslint-visitor-keys "^3.4.1" esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -3266,23 +3288,6 @@ eventsource@^1.0.7: resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2" integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== -extend-shallow@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" - integrity sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw== - dependencies: - kind-of "^1.1.0" - -fancy-log@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" - integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - parse-node-version "^1.0.0" - time-stamp "^1.0.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3293,10 +3298,10 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== +fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3314,15 +3319,20 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" + integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== + fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -3369,15 +3379,15 @@ filemanager-webpack-plugin@8.0.0: normalize-path "^3.0.0" schema-utils "^4.0.0" -filesize@10.0.7: - version "10.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.7.tgz#2237a816ee60a83fd0c3382ae70800e54eced3ad" - integrity sha512-iMRG7Qo9nayLoU3PNCiLizYtsy4W1ClrapeCwEgtiQelOAOuRJiw4QaLI+sSr8xr901dgHv+EYP2bCusGZgoiA== +filesize@10.1.6: + version "10.1.6" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361" + integrity sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3414,18 +3424,23 @@ find-up@^6.3.0: path-exists "^5.0.0" flat-cache@^3.0.4: - version "3.1.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" - integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.2.7" + flatted "^3.2.9" keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== focus-lock@^0.11.6: version "0.11.6" @@ -3441,6 +3456,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fork-ts-checker-webpack-plugin@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz#dae45dfe7298aa5d553e2580096ced79b6179504" @@ -3459,10 +3482,10 @@ fork-ts-checker-webpack-plugin@8.0.0: semver "^7.3.5" tapable "^2.2.1" -fraction.js@^4.2.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d" - integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg== +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fs-constants@^1.0.0: version "1.0.0" @@ -3479,9 +3502,9 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: universalify "^2.0.0" fs-monkey@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.4.tgz#ee8c1b53d3fe8bb7e5d2c5c5dfc0168afdd2f747" - integrity sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== fs.realpath@^1.0.0: version "1.0.0" @@ -3493,12 +3516,12 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.5: +function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== @@ -3518,28 +3541,30 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-node-dimensions@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -3560,6 +3585,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3572,16 +3609,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.3.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" - integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== - dependencies: - fs.realpath "^1.0.0" - minimatch "^8.0.2" - minipass "^4.2.4" - path-scurry "^1.6.1" - global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -3604,18 +3631,19 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.21.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" - integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== +globalthis@^1.0.3, globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - define-properties "^1.1.3" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.0.1, globby@^11.1.0: version "11.1.0" @@ -3634,13 +3662,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3648,16 +3669,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -3673,13 +3689,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== - dependencies: - ansi-regex "^2.0.0" - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3695,36 +3704,36 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: - has-symbols "^1.0.2" + has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" he@^1.2.0: version "1.2.0" @@ -3780,10 +3789,10 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== -html-webpack-plugin@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763" - integrity sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA== +html-webpack-plugin@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" + integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -3806,11 +3815,6 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== -https-browserify@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -3828,10 +3832,10 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== image-size@~0.5.0: version "0.5.5" @@ -3844,9 +3848,9 @@ immediate@~3.0.5: integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== "immutable@^3.8.1 || ^4.0.0", immutable@^4.0.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" - integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" @@ -3862,9 +3866,9 @@ import-lazy@^4.0.0: integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -3897,13 +3901,13 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + es-errors "^1.3.0" + hasown "^2.0.0" side-channel "^1.0.4" interpret@^3.1.1: @@ -3918,7 +3922,7 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -is-arguments@^1.0.4: +is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -3926,20 +3930,26 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" - integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: call-bind "^1.0.2" - get-intrinsic "^1.2.0" - is-typed-array "^1.1.10" + get-intrinsic "^1.2.1" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -3967,14 +3977,21 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.5.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: - has "^1.0.3" + hasown "^2.0.2" -is-date-object@^1.0.1: +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -3986,11 +4003,25 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -3998,10 +4029,15 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-number-object@^1.0.4: version "1.0.7" @@ -4042,7 +4078,7 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-regex@^1.0.4, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -4050,12 +4086,17 @@ is-regex@^1.0.4, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" @@ -4071,12 +4112,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.9: - version "1.1.12" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" - integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== dependencies: - which-typed-array "^1.1.11" + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2: version "1.0.2" @@ -4085,6 +4131,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-what@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" @@ -4115,10 +4169,23 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -isstream@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== +iterator.prototype@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" + integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" jdu@1.0.0: version "1.0.0" @@ -4135,14 +4202,14 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jiti@^1.18.2: - version "1.19.3" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.3.tgz#ef554f76465b3c2b222dc077834a71f0d4a37569" - integrity sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w== + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== -jquery@3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.0.tgz#fe2c01a05da500709006d8790fe21c8a39d75612" - integrity sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ== +jquery@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -4156,15 +4223,10 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@^3.0.2, jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== json-buffer@3.0.1: version "3.0.1" @@ -4228,17 +4290,12 @@ just-curry-it@^3.1.0: integrity sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg== keyv@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" -kind-of@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" - integrity sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g== - kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -4349,9 +4406,9 @@ loader-utils@^2.0.0: json5 "^2.1.2" loader-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + version "3.3.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== localforage@^1.8.1: version "1.10.0" @@ -4465,6 +4522,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.1.tgz#3a732fbfedb82c5ba7bca6564ad3f42afcb6e147" + integrity sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4479,11 +4541,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.0.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" - integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== - make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -4560,11 +4617,11 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0: @@ -4602,12 +4659,20 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.12.1" tiny-warning "^1.0.3" -mini-css-extract-plugin@2.7.5: - version "2.7.5" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5" - integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ== +mini-css-extract-plugin@2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758" + integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ== dependencies: schema-utils "^4.0.0" + tapable "^2.2.1" + +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" @@ -4623,10 +4688,10 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^8.0.2: - version "8.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" - integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -4651,15 +4716,10 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" - integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" - integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mkdirp@^0.5.6: version "0.5.6" @@ -4673,35 +4733,25 @@ mobile-detect@1.4.5: resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.5.tgz#da393c3c413ca1a9bcdd9ced653c38281c0fb6ad" integrity sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g== -moment@2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +moment@2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== mousetrap@1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" @@ -4709,11 +4759,10 @@ natural-compare@^1.4.0: integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== needle@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44" - integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" + integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== dependencies: - debug "^3.2.6" iconv-lite "^0.6.3" sax "^1.2.4" @@ -4742,10 +4791,10 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-releases@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-package-data@^2.5.0: version "2.5.0" @@ -4799,68 +4848,70 @@ object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" + call-bind "^1.0.7" + define-properties "^1.2.1" object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" + call-bind "^1.0.5" + define-properties "^1.2.1" has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" - integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -object.fromentries@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" - integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" -object.hasown@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" -object.values@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" - integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== +object.values@^1.1.6, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" once@^1.3.0, once@^1.4.0: version "1.4.0" @@ -4870,16 +4921,16 @@ once@^1.3.0, once@^1.4.0: wrappy "1" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" + word-wrap "^1.2.5" p-limit@^2.2.0: version "2.3.0" @@ -4935,6 +4986,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -4960,7 +5016,7 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-node-version@^1.0.0, parse-node-version@^1.0.1: +parse-node-version@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== @@ -4998,18 +5054,18 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.6.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== dependencies: - lru-cache "^9.1.1 || ^10.0.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + lru-cache "^11.0.0" + minipass "^7.1.2" path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== dependencies: isarray "0.0.1" @@ -5023,10 +5079,10 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" @@ -5052,17 +5108,6 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" -plugin-error@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" - integrity sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw== - dependencies: - ansi-cyan "^0.1.1" - ansi-red "^0.1.1" - arr-diff "^1.0.1" - arr-union "^2.0.1" - extend-shallow "^1.1.2" - popper.js@^1.14.4: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -5077,6 +5122,11 @@ portfinder@^1.0.17: debug "^3.2.7" mkdirp "^0.5.6" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-color-function@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" @@ -5133,23 +5183,23 @@ postcss-mixins@9.0.4: sugarss "^4.0.1" postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" - integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== dependencies: postcss-selector-parser "^6.0.4" @@ -5160,27 +5210,27 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== +postcss-nested@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== dependencies: - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.1.1" postcss-resolve-nested-selector@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" - integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== + version "0.1.6" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz#3d84dec809f34de020372c41b039956966896686" + integrity sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw== postcss-safe-parser@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== +postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -5215,14 +5265,14 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@8.4.47, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.32: + version "8.4.47" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" postcss@^6.0.23: version "6.0.23" @@ -5233,15 +5283,6 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23: - version "8.4.29" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd" - integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -5297,23 +5338,16 @@ psl@^1.1.33: integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.11.1: - version "6.11.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" - integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== +qs@6.13.0, qs@^6.4.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" - -qs@^6.4.0: - version "6.11.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" - integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== - dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" querystringify@^2.1.1: version "2.2.0" @@ -5478,11 +5512,6 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-lazyload@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-3.2.0.tgz#497bd06a6dbd7015e3376e1137a67dc47d2dd021" - integrity sha512-zJlrG8QyVZz4+xkYZH5v1w3YaP5wEFaYSUWC4CT9UXfK75IfRAIEdnyIUF+dXr3kX2MOtL1lUaZmaQZqrETwgw== - react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -5600,10 +5629,10 @@ react-virtualized@9.21.1: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react-window@1.8.8: - version "1.8.8" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243" - integrity sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ== +react-window@1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" @@ -5635,7 +5664,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.3.3: +readable-stream@^2.0.0, readable-stream@^2.0.5: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -5664,6 +5693,11 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -5724,10 +5758,23 @@ redux@4.2.1, redux@^4.0.0, redux@^4.1.1: dependencies: "@babel/runtime" "^7.9.2" -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== dependencies: regenerate "^1.4.2" @@ -5742,9 +5789,9 @@ regenerator-runtime@^0.11.0: integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regenerator-transform@^0.15.2: version "0.15.2" @@ -5753,33 +5800,58 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" - integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - functions-have-names "^1.2.3" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.2" -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== +regexpu-core@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.1.1.tgz#b469b245594cb2d088ceebc6369dceb8c00becac" + integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== dependencies: - "@babel/regjsgen" "^0.8.0" regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.11.0" unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== dependencies: - jsesc "~0.5.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.11.1.tgz#ae55c74f646db0c8fcb922d4da635e33da405149" + integrity sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ== + dependencies: + jsesc "~3.0.2" + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" relateurl@^0.2.7: version "0.2.7" @@ -5812,10 +5884,10 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@4.1.7: - version "4.1.7" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42" - integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A== +reselect@4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== reserved-words@^0.1.2: version "0.1.2" @@ -5849,21 +5921,21 @@ resolve-pathname@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" - integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.4: - version "2.0.0-next.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -5877,12 +5949,13 @@ rgb@~0.1.0: resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" integrity sha512-F49dXX73a92N09uQkfCp2QjwXpmJcn9/i9PvjmwsSIXUGqRLCf/yx5Q9gRxuLQTq248kakqQuc8GX/U/CxSqlA== -rimraf@4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" - integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== +rimraf@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" + integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== dependencies: - glob "^9.2.0" + glob "^11.0.0" + package-json-from-dist "^1.0.0" rimraf@^3.0.2: version "3.0.2" @@ -5898,22 +5971,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -run-sequence@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/run-sequence/-/run-sequence-2.2.1.tgz#1ce643da36fd8c7ea7e1a9329da33fc2b8898495" - integrity sha512-qkzZnQWMZjcKbh3CNly2srtrkaO/2H/SI5f2eliMCapdRD3UhMrwjfOAZJAnZ2H8Ju4aBzFZkBGXUqFs9V0yxw== +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - chalk "^1.1.3" - fancy-log "^1.3.2" - plugin-error "^0.1.2" - -safe-array-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" - integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.0" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" has-symbols "^1.0.3" isarray "^2.0.5" @@ -5932,13 +5996,13 @@ safe-json-parse@~1.0.1: resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" integrity sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A== -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" + call-bind "^1.0.6" + es-errors "^1.3.0" is-regex "^1.1.4" "safer-buffer@>= 2.1.2 < 3.0.0": @@ -5947,15 +6011,20 @@ safe-regex-test@^1.0.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.58.3: - version "1.66.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.66.1.tgz#04b51c4671e4650aa393740e66a4e58b44d055b1" - integrity sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA== + version "1.79.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.4.tgz#f9c45af35fbeb53d2c386850ec842098d9935267" + integrity sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg== dependencies: - chokidar ">=3.0.0 <4.0.0" + chokidar "^4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sax@^1.2.4, sax@~1.2.4: +sax@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + +sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -5997,35 +6066,50 @@ section-iterator@^2.0.0: resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" integrity sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ== -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" +semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.6.0: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -6055,14 +6139,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^4.0.1: version "4.1.0" @@ -6083,10 +6168,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-support@~0.5.20: version "0.5.21" @@ -6106,7 +6191,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.3, source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -6120,9 +6205,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" @@ -6133,9 +6218,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== stack-generator@^2.0.5: version "2.0.10" @@ -6166,20 +6251,12 @@ stacktrace-js@2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" -streamqueue@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/streamqueue/-/streamqueue-1.1.2.tgz#6c99c7c20d62b57f5819296bf9ec942542380192" - integrity sha512-CHUpqa+1BM99z7clQz9W6L9ZW4eXRRQCR0H+utVAGGvNo2ePlJAFjhdK0IjunaBbY/gWKJawk5kpJeyz0EXxRA== - dependencies: - isstream "^0.1.2" - readable-stream "^2.3.3" - string-template@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6188,46 +6265,77 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.8: - version "4.0.9" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.9.tgz#148779de0f75d36b13b15885fec5cadde994520d" - integrity sha512-6i5hL3MqG/K2G43mWXWgP+qizFW/QH/7kCNN13JrJS5q48FN5IKksLDscexKP3dnmB6cdm9jlNgAsWNLpSykmA== +string-width@^4.1.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.1.3" + es-abstract "^1.17.5" -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@0.10: version "0.10.31" @@ -6248,20 +6356,27 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -6289,12 +6404,12 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== -stylelint-order@6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.3.tgz#160b78650bd90463241b992581efee7159baefc2" - integrity sha512-1j1lOb4EU/6w49qZeT2SQVJXm0Ht+Qnq9GMfUa3pMwoyojIWfuA+JUDmoR97Bht1RLn4ei0xtLGy87M7d29B1w== +stylelint-order@6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.4.tgz#3e80d876c61a98d2640de181433686f24284748b" + integrity sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA== dependencies: - postcss "^8.4.21" + postcss "^8.4.32" postcss-sorting "^8.0.2" stylelint@15.6.1: @@ -6361,11 +6476,6 @@ sugarss@^4.0.1: resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-4.0.1.tgz#128a783ed71ee0fc3b489ce1f7d5a89bc1e24383" integrity sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw== -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== - supports-color@^5.3.0, supports-color@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -6388,9 +6498,9 @@ supports-color@^8.0.0: has-flag "^4.0.0" supports-hyperlinks@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" - integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz#b56150ff0173baacc15f21956450b61f2b18d3ac" + integrity sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -6406,9 +6516,9 @@ svg-tags@^1.0.0: integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== table@^6.8.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== dependencies: ajv "^8.0.1" lodash.truncate "^4.4.2" @@ -6432,21 +6542,21 @@ tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -terser-webpack-plugin@5.3.9, terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== +terser-webpack-plugin@5.3.10, terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.10.0, terser@^5.16.8: - version "5.19.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.3.tgz#359baeba615aef13db4b8c4d77a2aa0d8814aa9e" - integrity sha512-pQzJ9UJzM0IgmT4FAtYI6+VqFf0lj/to58AV0Xfgg0Up37RyPG7Al+1cepC6/BVuAxR9oNb41/DL4DEoHJvTdg== +terser@^5.10.0, terser@^5.26.0: + version "5.34.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6" + integrity sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -6458,20 +6568,10 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw== - -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== tiny-lr@^1.1.1: version "1.1.1" @@ -6521,10 +6621,15 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -6541,20 +6646,26 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -ts-loader@9.4.2: - version "9.4.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" - integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== +ts-api-utils@^1.3.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== + +ts-loader@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89" + integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" micromatch "^4.0.0" semver "^7.3.4" + source-map "^0.7.4" -tsconfig-paths@^3.14.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" - integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.2" @@ -6570,22 +6681,10 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -6614,44 +6713,49 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typed-array-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" - integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - is-typed-array "^1.1.10" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" -typed-array-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" - integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -typed-array-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" - integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" - has-proto "^1.0.1" - is-typed-array "^1.1.10" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" for-each "^0.3.3" - is-typed-array "^1.1.9" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" typed-styles@^0.0.7: version "0.0.7" @@ -6680,10 +6784,10 @@ typescript-plugin-css-modules@5.0.1: stylus "^0.59.0" tsconfig-paths "^4.1.2" -typescript@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" - integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== +typescript@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== unbox-primitive@^1.0.2: version "1.0.2" @@ -6695,10 +6799,15 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" @@ -6709,9 +6818,9 @@ unicode-match-property-ecmascript@^2.0.0: unicode-property-aliases-ecmascript "^2.0.0" unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" @@ -6724,17 +6833,17 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.2.0" + picocolors "^1.1.0" uri-js@^4.2.2: version "4.4.1" @@ -6761,9 +6870,9 @@ url-parse@^1.5.3: requires-port "^1.0.0" use-callback-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" - integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== dependencies: tslib "^2.0.0" @@ -6810,10 +6919,10 @@ warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -6853,11 +6962,12 @@ webpack-livereload-plugin@3.0.2: tiny-lr "^1.1.1" webpack-merge@^5.7.3: - version "5.9.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" - integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== dependencies: clone-deep "^4.0.1" + flat "^5.0.2" wildcard "^2.0.0" webpack-sources@^3.2.3: @@ -6865,34 +6975,33 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.89.0: - version "5.89.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" - integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== +webpack@5.95.0: + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1: @@ -6928,16 +7037,44 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.10, which-typed-array@^1.1.11: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-builtin-type@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.2" + which-typed-array "^1.1.15" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-tostringtag "^1.0.0" + has-tostringtag "^1.0.2" which@^1.3.1: version "1.3.1" @@ -6958,6 +7095,29 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -6972,9 +7132,9 @@ write-file-atomic@^5.0.1: signal-exit "^4.0.1" ws@^7.4.5: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xxhashjs@~0.2.2: version "0.2.2" @@ -7009,9 +7169,9 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== zip-stream@^4.1.0: version "4.1.1"