diff --git a/.devcontainer/Prowlarr.code-workspace b/.devcontainer/Prowlarr.code-workspace deleted file mode 100644 index a46158e44..000000000 --- a/.devcontainer/Prowlarr.code-workspace +++ /dev/null @@ -1,13 +0,0 @@ -// 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 deleted file mode 100644 index 70473224d..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,19 +0,0 @@ -// 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/.editorconfig b/.editorconfig index 2fc5f556b..12c25393b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,18 +36,9 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case dotnet_naming_style.instance_field_style.required_prefix = _ # Prefer "var" everywhere -csharp_style_var_for_built_in_types = true -csharp_style_var_when_type_is_apparent = true -csharp_style_var_elsewhere = true -# Prefer "out" variables to be declared inline -csharp_style_inlined_variable_declaration = true - -# Using directive is unnecessary. -dotnet_diagnostic.IDE0005.severity = error -# Use var instead of explicit type -dotnet_diagnostic.IDE0007.severity = error -# Inline variable declaration -dotnet_diagnostic.IDE0018.severity = error +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion # Stylecop Rules dotnet_diagnostic.SA0001.severity = none diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f70e2c23e..ee1c1e09d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first' +description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' labels: ['Type: Bug', 'Status: Needs Triage'] body: - type: checkboxes @@ -74,7 +74,7 @@ body: - type: checkboxes attributes: label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided. - description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace` + description: Trace logs are generally required for all bug reports options: - - label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue. + - label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue. required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index eb800532e..ad1dd9115 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,6 @@ contact_links: - name: Support via Discord url: https://prowlarr.com/discord about: Chat with users and devs on support and setup related topics. + - name: Support via Reddit + url: https://reddit.com/r/prowlarr + about: Discuss and search thru support topics. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f33a02cd1..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# 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/label-actions.yml b/.github/label-actions.yml index ce6d46c73..a5f466a16 100644 --- a/.github/label-actions.yml +++ b/.github/label-actions.yml @@ -4,21 +4,13 @@ comment: > :wave: @{issue-author}, we use the issue tracker exclusively for bug reports and feature requests. However, this issue appears - to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord). + to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord) + or [Subreddit](https://reddit.com/r/prowlarr) close: true - close-reason: 'not planned' 'Type: Indexer Request': comment: > :wave: @{issue-author}, we use the issue tracker exclusively for bug reports and feature requests. However, this issue appears to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/) - close: true - close-reason: 'not planned' - -'Status: Logs Needed': - comment: > - :wave: @{issue-author}, in order to help you further we'll need to see logs. - You'll need to enable trace logging and replicate the problem that you encountered. - Guidance on how to enable trace logging can be found in - our [troubleshooting guide](https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files). \ No newline at end of file + close: true \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 74160b634..000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,31 +0,0 @@ -'Area: API': - - changed-files: - - any-glob-to-any-file: - - src/Prowlarr.Api.V1/**/* - -'Area: Db-migration': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Datastore/Migration/* - -'Area: Download Clients': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Download/Clients/**/* - -'Area: Indexer': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Indexers/**/* - -'Area: Notifications': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Notifications/**/* - -'Area: UI': - - 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 77c35366c..8f35f6bd6 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@v4 + - uses: dessant/label-actions@v3 with: process-only: 'issues, prs' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index ab2292824..000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: "Pull Request Labeler" -on: - - pull_request_target - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v5 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 1d50cb1f1..cf38066c5 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@v5 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '90' diff --git a/.gitignore b/.gitignore index 689b44415..d903078ef 100644 --- a/.gitignore +++ b/.gitignore @@ -127,7 +127,6 @@ coverage*.xml coverage*.json setup/Output/ *.~is -.mono # VS outout folders bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 7a36fefe1..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-dotnettools.csdevkit", - "ms-vscode-remote.remote-containers" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index d13f9426e..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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 deleted file mode 100644 index b3e22f6d1..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "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 new file mode 100644 index 000000000..b879517cd --- /dev/null +++ b/Logo/dottrace.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/jetbrains.svg b/Logo/jetbrains.svg new file mode 100644 index 000000000..75d4d2177 --- /dev/null +++ b/Logo/jetbrains.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/resharper.svg b/Logo/resharper.svg new file mode 100644 index 000000000..24c987a78 --- /dev/null +++ b/Logo/resharper.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo/rider.svg b/Logo/rider.svg new file mode 100644 index 000000000..82da35b0b --- /dev/null +++ b/Logo/rider.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + rider + + + + + + + + + + + + + + diff --git a/Logo/webstorm.svg b/Logo/webstorm.svg new file mode 100644 index 000000000..39ab7eb97 --- /dev/null +++ b/Logo/webstorm.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index e8c60546a..e5759c632 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # 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) -[![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) +[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?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) [![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors) @@ -29,6 +29,7 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) +[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr) Note: GitHub Issues are for Bugs and Feature Requests Only @@ -68,16 +69,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## JetBrains -Thank you to [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. +Thank you to [JetBrains 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-2024 +- Copyright 2010-2022 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 dc667e803..6f778cb9c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,28 +9,24 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '1.35.0' + majorVersion: '1.4.0' minorVersion: $[counter('minorVersion', 1)] prowlarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' sentryOrg: 'servarr' sentryUrl: 'https://sentry.servarr.com' - dotnetVersion: '6.0.427' - nodeVersion: '20.X' - innoVersion: '6.2.2' + dotnetVersion: '6.0.408' + innoVersion: '6.2.0' + nodeVersion: '16.x' windowsImage: 'windows-2022' - linuxImage: 'ubuntu-22.04' - macImage: 'macOS-13' + linuxImage: 'ubuntu-20.04' + macImage: 'macOS-11' trigger: branches: include: - develop - master - paths: - exclude: - - .github - - src/Prowlarr.Api.*/openapi.json pr: branches: @@ -38,9 +34,8 @@ pr: - develop paths: exclude: - - .github - src/NzbDrone.Core/Localization/Core - - src/Prowlarr.Api.*/openapi.json + - src/Prowlarr.API.*/openapi.json stages: - stage: Setup @@ -166,10 +161,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -354,7 +349,7 @@ stages: includeRootFolder: false rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 - task: ArchiveFiles@2 - displayName: Create freebsd-x64 tar + displayName: Create FreeBSD Core Core tar inputs: archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz' archiveType: 'tar' @@ -367,7 +362,7 @@ stages: - bash: | echo "Uploading source maps to sentry" curl -sL https://sentry.io/get-cli/ | bash - RELEASENAME="Prowlarr@${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}" + RELEASENAME="${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}" sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}" sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite sentry-cli releases set-commits --auto "${RELEASENAME}" @@ -533,8 +528,8 @@ stages: testRunTitle: '$(testName) Unit Tests' failTaskOnFailedTests: true - - job: Unit_LinuxCore_Postgres14 - displayName: Unit Native LinuxCore with Postgres14 Database + - job: Unit_LinuxCore_Postgres + displayName: Unit Native LinuxCore with Postgres Database dependsOn: Prepare condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) variables: @@ -570,7 +565,6 @@ stages: -e POSTGRES_PASSWORD=prowlarr \ -e POSTGRES_USER=prowlarr \ -p 5432:5432/tcp \ - -v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \ postgres:14 displayName: Start postgres - bash: | @@ -583,60 +577,7 @@ stages: inputs: testResultsFormat: 'NUnit' testResultsFiles: '**/TestResult.xml' - testRunTitle: 'LinuxCore Postgres14 Unit Tests' - failTaskOnFailedTests: true - - - job: Unit_LinuxCore_Postgres15 - displayName: Unit Native LinuxCore with Postgres15 Database - dependsOn: Prepare - condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) - variables: - pattern: 'Prowlarr.*.linux-core-x64.tar.gz' - artifactName: linux-x64-tests - Prowlarr__Postgres__Host: 'localhost' - Prowlarr__Postgres__Port: '5432' - Prowlarr__Postgres__User: 'prowlarr' - Prowlarr__Postgres__Password: 'prowlarr' - - pool: - vmImage: ${{ variables.linuxImage }} - - timeoutInMinutes: 10 - - steps: - - task: UseDotNet@2 - displayName: 'Install .net core' - inputs: - version: $(dotnetVersion) - - checkout: none - - task: DownloadPipelineArtifact@2 - displayName: Download Test Artifact - inputs: - buildType: 'current' - artifactName: $(artifactName) - targetPath: $(testsFolder) - - bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \; - displayName: Make Test Dummy Executable - condition: and(succeeded(), ne(variables['osName'], 'Windows')) - - bash: | - docker run -d --name=postgres15 \ - -e POSTGRES_PASSWORD=prowlarr \ - -e POSTGRES_USER=prowlarr \ - -p 5432:5432/tcp \ - -v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \ - postgres:15 - displayName: Start postgres - - bash: | - chmod a+x ${TESTSFOLDER}/test.sh - ls -lR ${TESTSFOLDER} - ${TESTSFOLDER}/test.sh Linux Unit Test - displayName: Run Tests - - task: PublishTestResults@2 - displayName: Publish Test Results - inputs: - testResultsFormat: 'NUnit' - testResultsFiles: '**/TestResult.xml' - testRunTitle: 'LinuxCore Postgres15 Unit Tests' + testRunTitle: 'LinuxCore Postgres Unit Tests' failTaskOnFailedTests: true - stage: Integration @@ -722,8 +663,8 @@ stages: failTaskOnFailedTests: true displayName: Publish Test Results - - job: Integration_LinuxCore_Postgres14 - displayName: Integration Native LinuxCore with Postgres14 Database + - job: Integration_LinuxCore_Postgres + displayName: Integration Native LinuxCore with Postgres Database dependsOn: Prepare condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) variables: @@ -769,7 +710,6 @@ stages: -e POSTGRES_PASSWORD=prowlarr \ -e POSTGRES_USER=prowlarr \ -p 5432:5432/tcp \ - -v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \ postgres:14 displayName: Start postgres - bash: | @@ -780,70 +720,7 @@ stages: inputs: testResultsFormat: 'NUnit' testResultsFiles: '**/TestResult.xml' - testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests' - failTaskOnFailedTests: true - displayName: Publish Test Results - - - - job: Integration_LinuxCore_Postgres15 - displayName: Integration Native LinuxCore with Postgres Database - dependsOn: Prepare - condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) - variables: - pattern: 'Prowlarr.*.linux-core-x64.tar.gz' - Prowlarr__Postgres__Host: 'localhost' - Prowlarr__Postgres__Port: '5432' - Prowlarr__Postgres__User: 'prowlarr' - Prowlarr__Postgres__Password: 'prowlarr' - - pool: - vmImage: ${{ variables.linuxImage }} - - steps: - - task: UseDotNet@2 - displayName: 'Install .net core' - inputs: - version: $(dotnetVersion) - - checkout: none - - task: DownloadPipelineArtifact@2 - displayName: Download Test Artifact - inputs: - buildType: 'current' - artifactName: 'linux-x64-tests' - targetPath: $(testsFolder) - - task: DownloadPipelineArtifact@2 - displayName: Download Build Artifact - inputs: - buildType: 'current' - artifactName: Packages - itemPattern: '**/$(pattern)' - targetPath: $(Build.ArtifactStagingDirectory) - - task: ExtractFiles@1 - inputs: - archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' - destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' - displayName: Extract Package - - bash: | - mkdir -p ./bin/ - cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/ - displayName: Move Package Contents - - bash: | - docker run -d --name=postgres15 \ - -e POSTGRES_PASSWORD=prowlarr \ - -e POSTGRES_USER=prowlarr \ - -p 5432:5432/tcp \ - -v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \ - postgres:15 - displayName: Start postgres - - bash: | - chmod a+x ${TESTSFOLDER}/test.sh - ${TESTSFOLDER}/test.sh Linux Integration Test - displayName: Run Integration Tests - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'NUnit' - testResultsFiles: '**/TestResult.xml' - testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests' + testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests' failTaskOnFailedTests: true displayName: Publish Test Results @@ -1075,10 +952,10 @@ stages: pool: vmImage: $(imageName) steps: - - task: UseNode@1 + - task: NodeTool@0 displayName: Set Node.js version inputs: - version: $(nodeVersion) + versionSpec: $(nodeVersion) - checkout: self submodules: true fetchDepth: 1 @@ -1169,12 +1046,12 @@ stages: submodules: true - powershell: Set-Service SCardSvr -StartupType Manual displayName: Enable Windows Test Service - - task: SonarCloudPrepare@3 + - task: SonarCloudPrepare@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') inputs: SonarCloud: 'SonarCloud' organization: 'prowlarr' - scannerMode: 'dotnet' + scannerMode: 'MSBuild' projectKey: 'Prowlarr_Prowlarr' projectName: 'Prowlarr' projectVersion: '$(prowlarrVersion)' @@ -1187,21 +1064,25 @@ 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@3 + - task: SonarCloudAnalyze@1 condition: eq(variables['System.PullRequest.IsFork'], 'False') displayName: Publish SonarCloud Results - - task: reportgenerator@5.3.11 + - task: reportgenerator@4 displayName: Generate Coverage Report inputs: reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined' reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' - publishCodeCoverageResults: true + - task: PublishCodeCoverageResults@1 + displayName: Publish Coverage Report + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: './CoverageResults/combined/Cobertura.xml' + reportDirectory: './CoverageResults/combined/' - stage: Report_Out dependsOn: - Analyze - - Installer - Unit_Test - Integration - Automation diff --git a/build.sh b/build.sh index 5139dba52..9ce85b634 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.2}.exe" + curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe" mkdir _inno ./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno rm innosetup.exe @@ -392,21 +392,22 @@ then fi fi -if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]]; +if [ "$FRONTEND" = "YES" ]; then YarnInstall + RunWebpack fi if [ "$LINT" = "YES" ]; then + if [ -z "$FRONTEND" ]; + then + YarnInstall + fi + LintUI fi -if [ "$FRONTEND" = "YES" ]; -then - RunWebpack -fi - if [ "$PACKAGES" = "YES" ]; then UpdateVersionNumber diff --git a/docs.sh b/docs.sh old mode 100755 new mode 100644 index 38b0e0fbc..ae11bc83f --- a/docs.sh +++ b/docs.sh @@ -1,18 +1,13 @@ -#!/bin/bash -set -e - -FRAMEWORK="net6.0" PLATFORM=$1 -ARCHITECTURE="${2:-x64}" if [ "$PLATFORM" = "Windows" ]; then - RUNTIME="win-$ARCHITECTURE" + RUNTIME="win-x64" elif [ "$PLATFORM" = "Linux" ]; then - RUNTIME="linux-$ARCHITECTURE" + RUNTIME="linux-x64" elif [ "$PLATFORM" = "Mac" ]; then - RUNTIME="osx-$ARCHITECTURE" + RUNTIME="osx-x64" else - echo "Platform must be provided as first argument: Windows, Linux or Mac" + echo "Platform must be provided as first arguement: Windows, Linux or Mac" exit 1 fi @@ -26,23 +21,17 @@ 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 7.3.2 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli -dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & +dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 & -sleep 45 +sleep 30 kill %1 diff --git a/frontend/.csscomb.json b/frontend/.csscomb.json new file mode 100644 index 000000000..a82e49732 --- /dev/null +++ b/frontend/.csscomb.json @@ -0,0 +1,25 @@ +{ + "remove-empty-rulesets": true, + "always-semicolon": true, + "color-case": "lower", + "block-indent": " ", + "color-shorthand": false, + "element-case": "lower", + "eof-newline": true, + "leading-zero": true, + "quotes": "double", + "sort-order-fallback": "abc", + "space-before-colon": "", + "space-after-colon": " ", + "space-before-combinator": " ", + "space-after-combinator": " ", + "space-between-declarations": "\n", + "space-before-opening-brace": " ", + "space-after-opening-brace": "\n", + "space-after-selector-delimiter": " ", + "space-before-selector-delimiter": "", + "space-before-closing-brace": "\n", + "strip-spaces": true, + "tab-size": true, + "unitless-zero": false +} diff --git a/frontend/.esformatter b/frontend/.esformatter new file mode 100644 index 000000000..600bb0751 --- /dev/null +++ b/frontend/.esformatter @@ -0,0 +1,335 @@ +{ + "indent": { + "value": " ", + "FunctionExpression": 1, + "ArrayExpression": 1, + "ObjectExpression": 1 + }, + "lineBreak": { + "value": "\n", + + "before": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": ">=1", + "ArrowFunctionExpressionOpeningBrace": 0, + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": ">=1", + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": 0, + "CatchClosingBrace": ">=1", + "CatchKeyword": 0, + "CatchOpeningBrace": 0, + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": 0, + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": ">=1", + "DoWhileStatementOpeningBrace": 0, + "ElseIfStatement": 0, + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": 0, + "ElseStatement": 0, + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": 0, + "EmptyStatement": -1, + "EndOfFile": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": 0, + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 0, + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": "<2", + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 0, + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace":0, + "IIFEClosingParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": 0, + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": -1, + "MethodDefinition": ">=1", + "ObjectExpressionClosingBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": "<=2", + "PropertyValue": 0, + "ReturnStatement": -1, + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": 0, + "ThisExpression": -1, + "ThrowStatement": ">=1", + "TryClosingBrace": ">=1", + "TryKeyword": -1, + "TryOpeningBrace": 0, + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": 0, + "VariableDeclarationWithoutInit": ">=1", + "VariableName": ">=1", + "VariableValue": 0, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": 0 + }, + + "after": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": -1, + "ArrowFunctionExpressionOpeningBrace": ">=1", + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": -1, + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": -1, + "CatchClosingBrace": ">=0", + "CatchKeyword": 0, + "CatchOpeningBrace": ">=1", + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": ">=1", + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": 0, + "DoWhileStatementOpeningBrace": ">=1", + "ElseIfStatement": ">=1", + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": ">=1", + "ElseStatement": ">=1", + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": ">=1", + "EmptyStatement": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": ">=1", + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": -1, + "ForInStatementExpressionOpening": "<2", + "ForInStatementOpeningBrace": ">=1", + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": -1, + "ForStatementExpressionOpening": "<2", + "ForStatementOpeningBrace": ">=1", + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": ">=1", + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": -1, + "FunctionExpressionOpeningBrace": 1, + "IIFEOpeningParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": ">=1", + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": 0, + "MethodDefinition": ">=1", + "ObjectExpressionOpeningBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": -1, + "PropertyName": 0, + "ReturnStatement": -1, + "SwitchCaseColon": ">=1", + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": ">=1", + "ThisExpression": 0, + "ThrowStatement": ">=1", + "TryClosingBrace": 0, + "TryKeyword": -1, + "TryOpeningBrace": ">=1", + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": ">=1", + "VariableValue": -1, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": ">=1" + } + }, + "whiteSpace": { + "value": " ", + "removeTrailing": 1, + "before": { + "ArgumentComma": 0, + "ArgumentList": 0, + "ArgumentListArrayExpression": 0, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 0, + "ArrayExpressionOpening": 1, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 1, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 0, + "ConditionalExpressionAlternate": 1, + "ConditionalExpressionConsequent": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementConditional": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionClosingParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 1, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 0, + "ForStatementExpressionOpening": 1, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 0, + "FunctionDeclarationClosingBrace": 1, + "FunctionDeclarationOpeningBrace": 1, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace": 1, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 1, + "IfStatementOpeningBrace": 1, + "LineComment": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionClosing": 0, + "ObjectExpressionClosingBrace": 1, + "ParameterComma": 0, + "ParameterList": 0, + "Property": 1, + "PropertyName": 1, + "PropertyValue": 1, + "SwitchDiscriminantClosing": 0, + "SwitchDiscriminantOpening": 1, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "VariableValue": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 0, + "WhileStatementConditionalOpening": 1, + "WhileStatementOpeningBrace": 1 + }, + "after": { + "ArgumentComma": 1, + "ArgumentList": 0, + "ArgumentListArrayExpression": 1, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 1, + "ArrayExpressionOpening": 0, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 0, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 1, + "ConditionalExpressionConsequent": 1, + "ConditionalExpressionTest": 1, + "DoWhileStatementBody": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionOpeningParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 1, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 1, + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 1, + "FunctionDeclarationClosingBrace": 0, + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpressionClosingBrace": 0, + "FunctionExpressionOpeningBrace": 0, + "FunctionName": 0, + "FunctionReservedWord": 0, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 0, + "IfStatementOpeningBrace": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionOpening": 0, + "ObjectExpressionClosingBrace": 0, + "ObjectExpressionOpeningBrace": 1, + "ParameterComma": 1, + "ParameterList": 0, + "PropertyName": 0, + "PropertyValue": 0, + "SwitchDiscriminantClosing": 1, + "SwitchDiscriminantOpening": 0, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 1, + "WhileStatementConditionalOpening": 0, + "WhileStatementOpeningBrace": 1 + } + } +} diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 56eaaeaab..484650bb0 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -12,8 +12,6 @@ const dirs = fs .join('|'); module.exports = { - root: true, - parser: '@babel/eslint-parser', env: { @@ -26,8 +24,7 @@ module.exports = { globals: { expect: false, chai: false, - sinon: false, - JSX: true + sinon: false }, parserOptions: { @@ -357,16 +354,11 @@ module.exports = { ], rules: Object.assign(typescriptEslintRecommended.rules, { - '@typescript-eslint/no-unused-vars': [ - 'error', - { - args: 'after-used', - argsIgnorePattern: '^_', - ignoreRestSiblings: true - } - ], - '@typescript-eslint/explicit-function-return-type': 'off', 'no-shadow': 'off', + // These should be enabled after cleaning things up + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/prop-types': 'off', 'prettier/prettier': 'error', 'simple-import-sort/imports': [ 'error', @@ -379,41 +371,7 @@ 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/.jsbeautifyrc b/frontend/.jsbeautifyrc new file mode 100644 index 000000000..50aa6aa29 --- /dev/null +++ b/frontend/.jsbeautifyrc @@ -0,0 +1,12 @@ +{ + "js": { + "indent_size": 2, + "indent_char": " ", + "indent_level": 2, + "indent_with_tabs": false, + "preserve_newlines": true, + "brace_style": "collapse", + "max_preserve_newlines": 2, + "jslint_happy": true + } +} \ No newline at end of file diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc index f19357a4c..fb0d550bf 100644 --- a/frontend/.stylelintrc +++ b/frontend/.stylelintrc @@ -1,12 +1,12 @@ { - "plugins": [ - "stylelint-order" - ], - "ignoreFiles": [ - "frontend/src/Styles/scaffolding.css", - "**/*.js" - ], - "rules": { +"plugins": [ + "stylelint-order" +], +"ignoreFiles": [ + "frontend/src/Styles/scaffolding.css", + "**/*.js" +], +"rules": { "at-rule-empty-line-before": [ "always", { @@ -15,6 +15,9 @@ ] } ], + "at-rule-name-case": "lower", + "at-rule-name-newline-after": "always-multi-line", + "at-rule-name-space-after": "always", "at-rule-no-unknown": [ true, { @@ -25,36 +28,83 @@ } ], "at-rule-no-vendor-prefix": true, + "at-rule-semicolon-newline-after": "always", + "at-rule-semicolon-space-before": "never", + "block-closing-brace-empty-line-before": "never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always", + "block-closing-brace-space-after": "always-single-line", + "block-closing-brace-space-before": "always-single-line", "block-no-empty": true, + "block-opening-brace-newline-after": "always", + "block-opening-brace-newline-before": "never-single-line", + "block-opening-brace-space-after": "always-single-line", + "block-opening-brace-space-before": "always", + "color-hex-case": "lower", "color-hex-length": "short", "color-named": "never", "color-no-invalid-hex": true, "comment-whitespace-inside": "always", + "declaration-bang-space-after": "never", + "declaration-bang-space-before": "always", "declaration-block-no-duplicate-properties": [ true, { "ignoreProperties": [ - "composes" + "composes" ] } ], "declaration-block-no-redundant-longhand-properties": true, "declaration-block-no-shorthand-property-overrides": true, + "declaration-block-semicolon-newline-after": "always", + "declaration-block-semicolon-newline-before": "never-multi-line", + "declaration-block-semicolon-space-before": "never", "declaration-block-single-line-max-declarations": 1, + "declaration-block-trailing-semicolon": "always", + "declaration-colon-space-after": "always", + "declaration-colon-space-before": "never", "font-family-name-quotes": "always-unless-keyword", "function-calc-no-unspaced-operator": true, + "function-comma-newline-after": "never-multi-line", + "function-comma-newline-before": "never-multi-line", + "function-comma-space-after": "always", + "function-comma-space-before": "never", "function-linear-gradient-no-nonstandard-direction": true, "function-name-case": "lower", + "function-parentheses-newline-inside": "never-multi-line", + "function-parentheses-space-inside": "never", "function-url-quotes": "always", "function-url-scheme-disallowed-list": [ "data" ], + "function-whitespace-after": "always", + "indentation": 2, "keyframe-declaration-no-important": true, "length-zero-no-unit": true, + "max-empty-lines": 1, + "max-line-length": [ + 100, + { + "ignore": [ + "non-comments" + ] + } + ], "max-nesting-depth": 2, + "media-feature-colon-space-after": "always", + "media-feature-colon-space-before": "never", + "media-feature-name-case": "lower", "media-feature-name-no-vendor-prefix": true, + "media-feature-range-operator-space-after": "always", + "media-feature-range-operator-space-before": "always", "no-empty-source": true, + "no-eol-whitespace": true, + "no-extra-semicolons": true, "no-invalid-double-slash-comments": true, + "no-missing-end-of-source-newline": true, + "number-leading-zero": "always", + "number-no-trailing-zeros": true, "order/order": [ "custom-properties", "dollar-variables", @@ -82,7 +132,6 @@ "right", "bottom", "left", - "inset", "z-index", "display", "visibility", @@ -294,33 +343,54 @@ ] } ], + "property-case": "lower", "property-no-vendor-prefix": true, "rule-empty-line-before": [ "always", { "except": [ - "first-nested" + "first-nested" ], "ignore": [ - "after-comment" + "after-comment" ] } ], + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", "selector-attribute-quotes": "never", "selector-class-pattern": "^[A-Za-z0-9]+$", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-descendant-combinator-no-non-space": true, + "selector-list-comma-newline-after": "always", + "selector-list-comma-newline-before": "never-multi-line", + "selector-list-comma-space-before": "never", "selector-max-attribute": 0, "selector-max-class": 3, "selector-max-compound-selectors": 3, + "selector-max-empty-lines": 0, "selector-max-id": 0, "selector-max-universal": 0, + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-parentheses-space-inside": "never", + "selector-pseudo-element-case": "lower", "selector-pseudo-element-colon-notation": "double", "selector-pseudo-element-no-unknown": true, "selector-type-case": "lower", "selector-type-no-unknown": true, "shorthand-property-no-redundant-values": true, "string-no-newline": true, + "string-quotes": "single", "time-min-milliseconds": 100, + "unit-case": "lower", "unit-no-unknown": true, + "value-list-comma-newline-after": "never-multi-line", + "value-list-comma-newline-before": "never-multi-line", + "value-list-comma-space-after": "always", + "value-list-comma-space-before": "never", + "value-list-max-empty-lines": 0, "value-no-vendor-prefix": true } -} \ No newline at end of file +} diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json deleted file mode 100644 index 0e005a3cd..000000000 --- a/frontend/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "stylelint.vscode-stylelint", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ] -} \ No newline at end of file diff --git a/frontend/babel.config.js b/frontend/babel.config.js index ade9f24a2..4d60cc820 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -2,18 +2,16 @@ 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 }], - ['@babel/plugin-transform-nullish-coalescing-operator', { loose }], + ['@babel/plugin-proposal-optional-chaining', { loose }], + ['@babel/plugin-proposal-nullish-coalescing-operator', { loose }], // Stage 2 - '@babel/plugin-transform-export-namespace-from', + '@babel/plugin-proposal-export-namespace-from', // Stage 3 - ['@babel/plugin-transform-class-properties', { loose }], + ['@babel/plugin-proposal-class-properties', { loose }], '@babel/plugin-syntax-dynamic-import' ], env: { diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index ceacc4f04..385cd6481 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -25,7 +25,6 @@ module.exports = (env) => { const config = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'eval-source-map', - target: 'web', stats: { children: false @@ -36,7 +35,7 @@ module.exports = (env) => { }, entry: { - index: 'index.ts' + index: 'index.js' }, resolve: { @@ -51,7 +50,7 @@ module.exports = (env) => { 'node_modules' ], alias: { - jquery: 'jquery/dist/jquery.min' + jquery: 'jquery/src/jquery' }, fallback: { buffer: false, @@ -66,23 +65,23 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: isProduction ? '[name]-[contenthash].js' : '[name].js', + filename: '[name].js', sourceMapFilename: '[file].map' }, optimization: { moduleIds: 'deterministic', - chunkIds: isProduction ? 'deterministic' : 'named' + chunkIds: 'named', + splitChunks: { + chunks: 'initial', + name: 'vendors' + } }, performance: { hints: false }, - experiments: { - topLevelAwait: true - }, - plugins: [ new webpack.DefinePlugin({ __DEV__: !isProduction, @@ -90,15 +89,13 @@ module.exports = (env) => { }), new MiniCssExtractPlugin({ - filename: 'Content/styles.css', - chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' + filename: 'Content/styles.css' }), new HtmlWebpackPlugin({ template: 'frontend/src/index.ejs', filename: 'index.html', - publicPath: '/', - inject: false + publicPath: '/' }), new FileManagerPlugin({ @@ -170,7 +167,7 @@ module.exports = (env) => { loose: true, debug: false, useBuiltIns: 'entry', - corejs: '3.39' + corejs: 3 } ] ] @@ -191,7 +188,7 @@ module.exports = (env) => { options: { importLoaders: 1, modules: { - localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' + localIdentName: '[name]/[local]/[hash:base64:5]' } } }, @@ -254,19 +251,18 @@ module.exports = (env) => { config.resolve.alias['react-dom$'] = 'react-dom/profiling'; config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling'; - config.optimization = { - minimize: true, - minimizer: [ - new TerserPlugin({ - terserOptions: { - sourceMap: true, // Must be set to true if using source-maps in production - mangle: false, - keep_classnames: true, - keep_fnames: true - } - }) - ] - }; + config.optimization.minimizer = [ + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: true, // Must be set to true if using source-maps in production + terserOptions: { + mangle: false, + keep_classnames: true, + keep_fnames: true + } + }) + ]; } return config; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 89db00f8c..f657adf28 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -16,7 +16,6 @@ const mixinsFiles = [ module.exports = { plugins: [ - 'autoprefixer', ['postcss-mixins', { mixinsFiles }], diff --git a/frontend/.vscode/settings.json b/frontend/src/.vscode/settings.json similarity index 100% rename from frontend/.vscode/settings.json rename to frontend/src/.vscode/settings.json diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.js similarity index 57% rename from frontend/src/App/App.tsx rename to frontend/src/App/App.js index dba90a697..1eea6e082 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.js @@ -1,30 +1,31 @@ -import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; +import { ConnectedRouter } from 'connected-react-router'; +import PropTypes from 'prop-types'; 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'; -interface AppProps { - store: Store; - history: ConnectedRouterProps['history']; -} - -function App({ store, history }: AppProps) { +function App({ store, history }) { 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 new file mode 100644 index 000000000..0df7d2a49 --- /dev/null +++ b/frontend/src/App/AppRoutes.js @@ -0,0 +1,184 @@ +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 StatsConnector from 'Indexer/Stats/StatsConnector'; +import SearchIndexConnector from 'Search/SearchIndexConnector'; +import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector'; +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 deleted file mode 100644 index d451a12fb..000000000 --- a/frontend/src/App/AppRoutes.tsx +++ /dev/null @@ -1,117 +0,0 @@ -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 new file mode 100644 index 000000000..abc7f8832 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.js @@ -0,0 +1,30 @@ +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 deleted file mode 100644 index 696d36fb2..000000000 --- a/frontend/src/App/AppUpdatedModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -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 new file mode 100644 index 000000000..a21afbc5a --- /dev/null +++ b/frontend/src/App/AppUpdatedModalConnector.js @@ -0,0 +1,12 @@ +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.css b/frontend/src/App/AppUpdatedModalContent.css index 0df4183a6..37b89c9be 100644 --- a/frontend/src/App/AppUpdatedModalContent.css +++ b/frontend/src/App/AppUpdatedModalContent.css @@ -1,7 +1,6 @@ .version { margin: 0 3px; font-weight: bold; - font-family: var(--defaultFontFamily); } .maintenance { diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js new file mode 100644 index 000000000..d03609a69 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -0,0 +1,140 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +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 { 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 ( + + + Prowlarr Updated + + + +
+ Version {version} of Prowlarr has been installed, in order to get the latest changes you'll need to reload Prowlarr. +
+ + { + isPopulated && !error && !!update && +
+ { + !update.changes && +
+ {translate('MaintenanceRelease')} +
+ } + + { + !!update.changes && +
+
+ What's new? +
+ + + + +
+ } +
+ } + + { + !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 deleted file mode 100644 index 0bd5df6d3..000000000 --- a/frontend/src/App/AppUpdatedModalContent.tsx +++ /dev/null @@ -1,145 +0,0 @@ -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 new file mode 100644 index 000000000..97dd0aeb9 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -0,0 +1,78 @@ +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 new file mode 100644 index 000000000..bd4d6a6c8 --- /dev/null +++ b/frontend/src/App/ApplyTheme.js @@ -0,0 +1,50 @@ +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 deleted file mode 100644 index ec9cd037f..000000000 --- a/frontend/src/App/ApplyTheme.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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.ts b/frontend/src/App/ColorImpairedContext.js similarity index 100% rename from frontend/src/App/ColorImpairedContext.ts rename to frontend/src/App/ColorImpairedContext.js diff --git a/frontend/src/App/ConnectionLostModal.tsx b/frontend/src/App/ConnectionLostModal.js similarity index 51% rename from frontend/src/App/ConnectionLostModal.tsx rename to frontend/src/App/ConnectionLostModal.js index f08f2c0e2..16adf78f5 100644 --- a/frontend/src/App/ConnectionLostModal.tsx +++ b/frontend/src/App/ConnectionLostModal.js @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; @@ -9,31 +10,36 @@ import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './ConnectionLostModal.css'; -interface ConnectionLostModalProps { - isOpen: boolean; -} - -function ConnectionLostModal(props: ConnectionLostModalProps) { - const { isOpen } = props; - - const handleModalClose = useCallback(() => { - location.reload(); - }, []); +function ConnectionLostModal(props) { + const { + isOpen, + onModalClose + } = props; return ( - - - {translate('ConnectionLost')} + + + + {translate('ConnectionLost')} + -
{translate('ConnectionLostToBackend')}
+
+ {translate('ConnectionLostMessage')} +
- {translate('ConnectionLostReconnect')} + {translate('ConnectionLostAutomaticMessage')}
- @@ -42,4 +48,9 @@ function ConnectionLostModal(props: ConnectionLostModalProps) { ); } +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 new file mode 100644 index 000000000..8ab8e3cd0 --- /dev/null +++ b/frontend/src/App/ConnectionLostModalConnector.js @@ -0,0 +1,12 @@ +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/SelectContext.tsx b/frontend/src/App/SelectContext.tsx index 66be388ce..6980129c1 100644 --- a/frontend/src/App/SelectContext.tsx +++ b/frontend/src/App/SelectContext.tsx @@ -1,28 +1,58 @@ import { cloneDeep } from 'lodash'; -import React, { useCallback, useEffect } from 'react'; -import useSelectState, { SelectState } from 'Helpers/Hooks/useSelectState'; +import React, { useEffect } from 'react'; +import areAllSelected from 'Utilities/Table/areAllSelected'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; import ModelBase from './ModelBase'; -export type SelectContextAction = - | { type: 'reset' } - | { type: 'selectAll' } - | { type: 'unselectAll' } +export enum SelectActionType { + Reset, + SelectAll, + UnselectAll, + ToggleSelected, + RemoveItem, + UpdateItems, +} + +type SelectedState = Record; + +interface SelectState { + selectedState: SelectedState; + lastToggled: number | null; + allSelected: boolean; + allUnselected: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + items: any[]; +} + +type SelectAction = + | { type: SelectActionType.Reset } + | { type: SelectActionType.SelectAll } + | { type: SelectActionType.UnselectAll } | { - type: 'toggleSelected'; + type: SelectActionType.ToggleSelected; id: number; isSelected: boolean; shiftKey: boolean; } | { - type: 'removeItem'; + type: SelectActionType.RemoveItem; id: number; } | { - type: 'updateItems'; + type: SelectActionType.UpdateItems; items: ModelBase[]; }; -export type SelectDispatch = (action: SelectContextAction) => void; +type Dispatch = (action: SelectAction) => void; + +const initialState = { + selectedState: {}, + lastToggled: null, + allSelected: false, + allUnselected: true, + items: [], +}; interface SelectProviderOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -30,40 +60,90 @@ interface SelectProviderOptions { items: Array; } -const SelectContext = React.createContext< - [SelectState, SelectDispatch] | undefined ->(cloneDeep(undefined)); +function getSelectedState(items: ModelBase[], existingState: SelectedState) { + return items.reduce((acc: SelectedState, item) => { + const id = item.id; + + acc[id] = existingState[id] ?? false; + + return acc; + }, {}); +} + +// TODO: Can this be reused? + +const SelectContext = React.createContext<[SelectState, Dispatch] | undefined>( + cloneDeep(undefined) +); + +function selectReducer(state: SelectState, action: SelectAction): SelectState { + const { items, selectedState } = state; + + switch (action.type) { + case SelectActionType.Reset: { + return cloneDeep(initialState); + } + case SelectActionType.SelectAll: { + return { + items, + ...selectAll(selectedState, true), + }; + } + case SelectActionType.UnselectAll: { + return { + items, + ...selectAll(selectedState, false), + }; + } + case SelectActionType.ToggleSelected: { + const result = { + items, + ...toggleSelected( + state, + items, + action.id, + action.isSelected, + action.shiftKey + ), + }; + + return result; + } + case SelectActionType.UpdateItems: { + const nextSelectedState = getSelectedState(action.items, selectedState); + + return { + ...state, + ...areAllSelected(nextSelectedState), + selectedState: nextSelectedState, + items: action.items, + }; + } + default: { + throw new Error(`Unhandled action type: ${action.type}`); + } + } +} export function SelectProvider( props: SelectProviderOptions ) { const { items } = props; - const [state, dispatch] = useSelectState(); + const selectedState = getSelectedState(items, {}); - const dispatchWrapper = useCallback( - (action: SelectContextAction) => { - switch (action.type) { - case 'reset': - case 'removeItem': - dispatch(action); - break; + const [state, dispatch] = React.useReducer(selectReducer, { + selectedState, + lastToggled: null, + allSelected: false, + allUnselected: true, + items, + }); - default: - dispatch({ - ...action, - items, - }); - break; - } - }, - [items, dispatch] - ); - - const value: [SelectState, SelectDispatch] = [state, dispatchWrapper]; + const value: [SelectState, Dispatch] = [state, dispatch]; useEffect(() => { - dispatch({ type: 'updateItems', items }); - }, [items, dispatch]); + dispatch({ type: SelectActionType.UpdateItems, items }); + }, [items]); return ( diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts deleted file mode 100644 index f89eb25f7..000000000 --- a/frontend/src/App/State/AppSectionState.ts +++ /dev/null @@ -1,63 +0,0 @@ -import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp, PropertyFilter } from './AppState'; - -export interface Error { - responseJSON: { - message: string; - }; -} - -export interface AppSectionDeleteState { - isDeleting: boolean; - deleteError: Error; -} - -export interface AppSectionSaveState { - isSaving: boolean; - saveError: Error; -} - -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 { - isSchemaFetching: boolean; - isSchemaPopulated: boolean; - schemaError: Error; - schema: { - items: T[]; - }; -} - -export interface AppSectionItemState { - isFetching: boolean; - isPopulated: boolean; - error: Error; - pendingChanges: Partial; - item: T; -} - -interface AppSectionState { - isFetching: boolean; - isPopulated: boolean; - error: Error; - items: T[]; - sortKey: string; - sortDirection: SortDirection; -} - -export default AppSectionState; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts deleted file mode 100644 index 0f0e82c0d..000000000 --- a/frontend/src/App/State/AppState.ts +++ /dev/null @@ -1,71 +0,0 @@ -import CommandAppState from './CommandAppState'; -import HistoryAppState from './HistoryAppState'; -import IndexerAppState, { - IndexerHistoryAppState, - IndexerIndexAppState, - IndexerStatusAppState, -} from './IndexerAppState'; -import IndexerStatsAppState from './IndexerStatsAppState'; -import SettingsAppState from './SettingsAppState'; -import SystemAppState from './SystemAppState'; -import TagsAppState from './TagsAppState'; - -interface FilterBuilderPropOption { - id: string; - name: string; -} - -export interface FilterBuilderProp { - name: string; - label: string; - type: string; - valueType?: string; - optionsSelector?: (items: T[]) => FilterBuilderPropOption[]; -} - -export interface PropertyFilter { - key: string; - value: boolean | string | number | string[] | number[]; - type: string; -} - -export interface Filter { - key: string; - label: string; - filers: PropertyFilter[]; -} - -export interface CustomFilter { - id: number; - type: string; - label: string; - 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; - indexerIndex: IndexerIndexAppState; - indexerStats: IndexerStatsAppState; - indexerStatus: IndexerStatusAppState; - indexers: IndexerAppState; - settings: SettingsAppState; - system: SystemAppState; - tags: TagsAppState; -} - -export default AppState; diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts deleted file mode 100644 index f4110ef73..000000000 --- a/frontend/src/App/State/ClientSideCollectionAppState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CustomFilter } from './AppState'; - -interface ClientSideCollectionAppState { - totalItems: number; - customFilters: CustomFilter[]; -} - -export default ClientSideCollectionAppState; diff --git a/frontend/src/App/State/CommandAppState.ts b/frontend/src/App/State/CommandAppState.ts deleted file mode 100644 index 1bde37371..000000000 --- a/frontend/src/App/State/CommandAppState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import AppSectionState from 'App/State/AppSectionState'; -import Command from 'Commands/Command'; - -export type CommandAppState = AppSectionState; - -export default CommandAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts deleted file mode 100644 index 3bb0e85f5..000000000 --- a/frontend/src/App/State/HistoryAppState.ts +++ /dev/null @@ -1,14 +0,0 @@ -import AppSectionState, { - AppSectionFilterState, -} from 'App/State/AppSectionState'; -import Column from 'Components/Table/Column'; -import History from 'typings/History'; - -interface HistoryAppState - extends AppSectionState, - AppSectionFilterState { - pageSize: number; - columns: Column[]; -} - -export default HistoryAppState; diff --git a/frontend/src/App/State/IndexerAppState.ts b/frontend/src/App/State/IndexerAppState.ts deleted file mode 100644 index 4c0145d0d..000000000 --- a/frontend/src/App/State/IndexerAppState.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; -import Indexer, { IndexerStatus } from 'Indexer/Indexer'; -import History from 'typings/History'; -import AppSectionState, { - AppSectionDeleteState, - AppSectionSaveState, -} from './AppSectionState'; -import { Filter, FilterBuilderProp } from './AppState'; - -export interface IndexerIndexAppState { - isTestingAll: boolean; - sortKey: string; - sortDirection: SortDirection; - secondarySortKey: string; - secondarySortDirection: SortDirection; - view: string; - - tableOptions: { - showSearchAction: boolean; - }; - - selectedFilterKey: string; - filterBuilderProps: FilterBuilderProp[]; - filters: Filter[]; - columns: Column[]; -} - -interface IndexerAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState { - itemMap: Record; - - isTestingAll: boolean; -} - -export type IndexerStatusAppState = AppSectionState; - -export type IndexerHistoryAppState = AppSectionState; - -export default IndexerAppState; diff --git a/frontend/src/App/State/IndexerStatsAppState.ts b/frontend/src/App/State/IndexerStatsAppState.ts deleted file mode 100644 index 8d3ae660a..000000000 --- a/frontend/src/App/State/IndexerStatsAppState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AppSectionItemState } from 'App/State/AppSectionState'; -import { Filter, FilterBuilderProp } from 'App/State/AppState'; -import Indexer from 'Indexer/Indexer'; -import { IndexerStats } from 'typings/IndexerStats'; - -export interface IndexerStatsAppState - extends AppSectionItemState { - filterBuilderProps: FilterBuilderProp[]; - selectedFilterKey: string; - filters: Filter[]; -} - -export default IndexerStatsAppState; diff --git a/frontend/src/App/State/ReleaseAppState.ts b/frontend/src/App/State/ReleaseAppState.ts deleted file mode 100644 index 325a429fa..000000000 --- a/frontend/src/App/State/ReleaseAppState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, -} from 'App/State/AppSectionState'; -import Release from 'typings/Release'; - -interface ReleaseAppState - extends AppSectionState, - AppSectionDeleteState {} - -export default ReleaseAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts deleted file mode 100644 index 33c6c936d..000000000 --- a/frontend/src/App/State/SettingsAppState.ts +++ /dev/null @@ -1,57 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, - 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 General from 'typings/Settings/General'; -import UiSettings from 'typings/Settings/UiSettings'; - -export interface AppProfileAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -export interface ApplicationAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} - -export interface DownloadClientAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} - -export interface GeneralAppState - extends AppSectionItemState, - AppSectionSaveState {} - -export interface IndexerCategoryAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -export interface NotificationAppState - extends AppSectionState, - AppSectionDeleteState {} - -export type UiSettingsAppState = AppSectionItemState; - -interface SettingsAppState { - appProfiles: AppProfileAppState; - applications: ApplicationAppState; - downloadClients: DownloadClientAppState; - general: GeneralAppState; - indexerCategories: IndexerCategoryAppState; - notifications: NotificationAppState; - ui: UiSettingsAppState; -} - -export default SettingsAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts deleted file mode 100644 index 8bc1b03e2..000000000 --- a/frontend/src/App/State/SystemAppState.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Health from 'typings/Health'; -import SystemStatus from 'typings/SystemStatus'; -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/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts deleted file mode 100644 index 53a0d847f..000000000 --- a/frontend/src/App/State/TagsAppState.ts +++ /dev/null @@ -1,28 +0,0 @@ -import ModelBase from 'App/ModelBase'; -import AppSectionState, { - AppSectionDeleteState, - AppSectionSaveState, -} from 'App/State/AppSectionState'; - -export interface Tag extends ModelBase { - label: string; -} - -export interface TagDetail extends ModelBase { - label: string; - applicationIds: number[]; - indexerIds: number[]; - indexerProxyIds: number[]; - notificationIds: number[]; -} - -export interface TagDetailAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -interface TagsAppState extends AppSectionState, AppSectionDeleteState { - details: TagDetailAppState; -} - -export default TagsAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts deleted file mode 100644 index b9b31bf63..000000000 --- a/frontend/src/Commands/Command.ts +++ /dev/null @@ -1,36 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export interface CommandBody { - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - completionMessage: string; - requiresDiskAccess: boolean; - isExclusive: boolean; - isLongRunning: boolean; - name: string; - lastExecutionTime: string; - lastStartTime: string; - trigger: string; - suppressMessages: boolean; -} - -interface Command extends ModelBase { - name: string; - commandName: string; - message: string; - body: CommandBody; - priority: string; - status: string; - result: string; - queued: string; - started: string; - ended: string; - duration: string; - trigger: string; - stateChangeTime: string; - sendUpdatesToClient: boolean; - updateScheduledTask: boolean; - lastExecutionTime: string; -} - -export default Command; diff --git a/frontend/src/Components/Chart/BarChart.js b/frontend/src/Components/Chart/BarChart.js index 83176c989..b9d7f0acc 100644 --- a/frontend/src/Components/Chart/BarChart.js +++ b/frontend/src/Components/Chart/BarChart.js @@ -2,7 +2,6 @@ import Chart from 'chart.js/auto'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { kinds } from 'Helpers/Props'; -import { defaultFontFamily } from 'Styles/Variables/fonts'; function getColors(kind) { @@ -40,15 +39,7 @@ class BarChart extends Component { plugins: { title: { display: true, - align: 'start', - text: this.props.title, - padding: { - bottom: 30 - }, - font: { - size: 14, - family: defaultFontFamily - } + text: this.props.title }, legend: { display: this.props.legend diff --git a/frontend/src/Components/Chart/DoughnutChart.js b/frontend/src/Components/Chart/DoughnutChart.js index d10979aa1..dd5052e23 100644 --- a/frontend/src/Components/Chart/DoughnutChart.js +++ b/frontend/src/Components/Chart/DoughnutChart.js @@ -1,7 +1,6 @@ import Chart from 'chart.js/auto'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { defaultFontFamily } from 'Styles/Variables/fonts'; function getColors(kind) { @@ -23,15 +22,7 @@ class DoughnutChart extends Component { plugins: { title: { display: true, - align: 'start', - text: this.props.title, - padding: { - bottom: 30 - }, - font: { - size: 14, - family: defaultFontFamily - } + text: this.props.title }, legend: { position: 'bottom' diff --git a/frontend/src/Components/Chart/StackedBarChart.js b/frontend/src/Components/Chart/StackedBarChart.js index b69fd8e03..d6e4879d2 100644 --- a/frontend/src/Components/Chart/StackedBarChart.js +++ b/frontend/src/Components/Chart/StackedBarChart.js @@ -1,7 +1,6 @@ import Chart from 'chart.js/auto'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { defaultFontFamily } from 'Styles/Variables/fonts'; function getColors(index) { @@ -37,19 +36,7 @@ class StackedBarChart extends Component { plugins: { title: { display: true, - align: 'start', - text: this.props.title, - padding: { - bottom: 30 - }, - font: { - size: 14, - family: defaultFontFamily - } - }, - tooltip: { - mode: 'index', - position: 'average' + text: this.props.title } } }, diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js index 931557045..39f634cc9 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItem.js +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -10,7 +10,6 @@ class DescriptionListItem extends Component { render() { const { - className, titleClassName, descriptionClassName, title, @@ -18,7 +17,7 @@ class DescriptionListItem extends Component { } = this.props; return ( -
+ @@ -30,13 +29,12 @@ class DescriptionListItem extends Component { > {data} -
+ ); } } 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 51d286311..b3db237b1 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -23,9 +23,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) { info, } = props; - const [detailedError, setDetailedError] = useState< - StackTrace.StackFrame[] | null - >(null); + const [detailedError, setDetailedError] = useState(null); useEffect(() => { if (error) { @@ -63,7 +61,11 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
{info.componentStack}
)} -
Version: {window.Prowlarr.version}
+ { +
+ Version: {window.Prowlarr.version} +
+ } ); diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js index dfb720003..3c8aa81a6 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; import Alert from 'Components/Alert'; import PathInput from 'Components/Form/PathInput'; import Button from 'Components/Link/Button'; @@ -20,12 +21,12 @@ import styles from './FileBrowserModalContent.css'; const columns = [ { name: 'type', - label: () => translate('Type'), + label: translate('Type'), isVisible: true }, { name: 'name', - label: () => translate('Name'), + label: translate('Name'), isVisible: true } ]; @@ -38,7 +39,7 @@ class FileBrowserModalContent extends Component { constructor(props, context) { super(props, context); - this._scrollerRef = React.createRef(); + this._scrollerNode = null; this.state = { isFileBrowserModalOpen: false, @@ -56,10 +57,21 @@ class FileBrowserModalContent extends Component { currentPath !== prevState.currentPath ) { this.setState({ currentPath }); - this._scrollerRef.current.scrollTop = 0; + this._scrollerNode.scrollTop = 0; } } + // + // Control + + setScrollerRef = (ref) => { + if (ref) { + this._scrollerNode = ReactDOM.findDOMNode(ref); + } else { + this._scrollerNode = null; + } + }; + // // Listeners @@ -133,7 +145,7 @@ class FileBrowserModalContent extends Component { /> diff --git a/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx deleted file mode 100644 index 6a7dddcfc..000000000 --- a/frontend/src/Components/Filter/Builder/CategoryFilterBuilderRowValue.tsx +++ /dev/null @@ -1,41 +0,0 @@ -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 0c4a31657..033b9a69a 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js @@ -1,4 +1,3 @@ -import { maxBy } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -51,7 +50,7 @@ class FilterBuilderModalContent extends Component { if (id) { dispatchSetFilter({ selectedFilterKey: id }); } else { - const last = maxBy(customFilters, 'id'); + const last = customFilters[customFilters.length -1]; dispatchSetFilter({ selectedFilterKey: last.id }); } @@ -109,7 +108,7 @@ class FilterBuilderModalContent extends Component { this.setState({ labelErrors: [ { - message: translate('LabelIsRequired') + message: 'Label is required' } ] }); @@ -147,13 +146,13 @@ class FilterBuilderModalContent extends Component { return ( - {translate('CustomFilter')} + Custom Filter
- {translate('Label')} + Label
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index b02844c61..ed375b745 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,13 +3,10 @@ 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'; @@ -58,15 +55,9 @@ 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; @@ -207,13 +198,11 @@ class FilterBuilderRow extends Component { const selectedFilterBuilderProp = this.selectedFilterBuilderProp; const keyOptions = filterBuilderProps.map((availablePropFilter) => { - const { name, label } = availablePropFilter; - return { - key: name, - value: typeof label === 'function' ? label() : label + key: availablePropFilter.name, + value: availablePropFilter.label }; - }).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 d1419327a..a7aed80b6 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 sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByProp('name')); + }, []).sort(sortByName); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts deleted file mode 100644 index 5bf9e5785..000000000 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 03c5f7227..000000000 --- a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx +++ /dev/null @@ -1,39 +0,0 @@ -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/Builder/IndexerFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js index fc211caec..bb4e594cc 100644 --- a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js @@ -9,13 +9,13 @@ import FilterBuilderRowValue from './FilterBuilderRowValue'; function createMapStateToProps() { return createSelector( (state) => state.indexers, - (indexers) => { + (qualityProfiles) => { const { isFetching, isPopulated, error, items - } = indexers; + } = qualityProfiles; const tagList = items.map((item) => { return { diff --git a/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js index 4f6250151..4004f0ced 100644 --- a/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js @@ -3,24 +3,9 @@ import translate from 'Utilities/String/translate'; import FilterBuilderRowValue from './FilterBuilderRowValue'; const privacyTypes = [ - { - id: 'public', - get name() { - return translate('Public'); - } - }, - { - id: 'private', - get name() { - return translate('Private'); - } - }, - { - id: 'semiPrivate', - get name() { - return translate('SemiPrivate'); - } - } + { id: 'public', name: translate('Public') }, + { id: 'private', name: translate('Private') }, + { id: 'semiPrivate', name: translate('SemiPrivate') } ]; function PrivacyFilterBuilderRowValue(props) { diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index 9f378d5a2..7407f729a 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 deletion was successful. - // Moving this check to an ancestor would be more accurate, but would have + // Assume that delete and then unmounting means the delete was successful. + // Moving this check to a 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 99cb6ec5c..07660426e 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,7 +5,6 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -31,24 +30,22 @@ function CustomFiltersModalContent(props) { { - customFilters - .sort((a, b) => sortByProp(a, b, 'label')) - .map((customFilter) => { - return ( - - ); - }) + customFilters.map((customFilter) => { + return ( + + ); + }) }
diff --git a/frontend/src/Components/Form/AppProfileSelectInputConnector.js b/frontend/src/Components/Form/AppProfileSelectInputConnector.js index 0ab181e2f..fc40e9d3c 100644 --- a/frontend/src/Components/Form/AppProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/AppProfileSelectInputConnector.js @@ -4,13 +4,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; +import sortByName from 'Utilities/Array/sortByName'; import SelectInput from './SelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByProp('name')), + createSortedSectionSelector('settings.appProfiles', sortByName), (state, { includeNoChange }) => includeNoChange, (state, { includeMixed }) => includeMixed, (appProfiles, includeNoChange, includeMixed) => { @@ -24,20 +23,16 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: true + value: 'No Change', + disabled: true }); } if (includeMixed) { values.unshift({ key: 'mixed', - get value() { - return `(${translate('Mixed')})`; - }, - isDisabled: true + value: '(Mixed)', + disabled: true }); } diff --git a/frontend/src/Components/Form/AvailabilitySelectInput.js b/frontend/src/Components/Form/AvailabilitySelectInput.js new file mode 100644 index 000000000..af9bdb2d6 --- /dev/null +++ b/frontend/src/Components/Form/AvailabilitySelectInput.js @@ -0,0 +1,54 @@ +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 deleted file mode 100644 index 9cf7a429a..000000000 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ /dev/null @@ -1,100 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchDownloadClients } from 'Store/Actions/settingsActions'; -import sortByProp from 'Utilities/Array/sortByProp'; -import translate from 'Utilities/String/translate'; -import EnhancedSelectInput from './EnhancedSelectInput'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.downloadClients, - (state, { includeAny }) => includeAny, - (state, { protocol }) => protocol, - (downloadClients, includeAny, protocolFilter) => { - const { - isFetching, - isPopulated, - error, - items - } = downloadClients; - - const values = items - .filter((downloadClient) => downloadClient.protocol === protocolFilter) - .sort(sortByProp('name')) - .map((downloadClient) => ({ - key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` - })); - - if (includeAny) { - values.unshift({ - key: 0, - value: `(${translate('Any')})` - }); - } - - return { - isFetching, - isPopulated, - error, - values - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchDownloadClients: fetchDownloadClients -}; - -class DownloadClientSelectInputConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.dispatchFetchDownloadClients(); - } - } - - // - // Listeners - - onChange = ({ name, value }) => { - this.props.onChange({ name, value: parseInt(value) }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -DownloadClientSelectInputConnector.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - values: PropTypes.arrayOf(PropTypes.object).isRequired, - includeAny: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired -}; - -DownloadClientSelectInputConnector.defaultProps = { - includeAny: false, - protocol: 'torrent' -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector); diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 79b1c999c..4df54092c 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.js +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -20,8 +20,6 @@ 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; } @@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component { // Listeners onComputeMaxHeight = (data) => { + const { + top, + bottom + } = data.offsets.reference; + const windowHeight = window.innerHeight; - data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } return data; }; @@ -264,29 +271,26 @@ class EnhancedSelectInput extends Component { this.setState({ isOpen: !this.state.isOpen }); }; - onSelect = (newValue) => { - const { name, value, values, onChange } = this.props; - - if (Array.isArray(value)) { - let arrayValue = null; - const index = value.indexOf(newValue); - + onSelect = (value) => { + if (Array.isArray(this.props.value)) { + let newValue = null; + const index = this.props.value.indexOf(value); if (index === -1) { - arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v)); + newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); } else { - arrayValue = [...value]; - arrayValue.splice(index, 1); + newValue = [...this.props.value]; + newValue.splice(index, 1); } - onChange({ - name, - value: arrayValue + this.props.onChange({ + name: this.props.name, + value: newValue }); } else { this.setState({ isOpen: false }); - onChange({ - name, - value: newValue + this.props.onChange({ + name: this.props.name, + value }); } }; @@ -453,10 +457,6 @@ 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 && Array.isArray(value) && value.includes(v.parentKey); + const parentSelected = hasParent && value.includes(v.parentKey); return ( {error.errorMessage} - - { - error.detailedDescription ? - } - tooltip={error.detailedDescription} - kind={kinds.INVERSE} - position={tooltipPositions.TOP} - /> : - null - } ); }) @@ -53,18 +39,6 @@ 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 new file mode 100644 index 000000000..a7145363a --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.js @@ -0,0 +1,54 @@ +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/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js new file mode 100644 index 000000000..a82c8889f --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; +import { align, icons, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './PageHeaderActionsMenu.css'; + +function PageHeaderActionsMenu(props) { + const { + formsAuth, + onKeyboardShortcutsPress, + onRestartPress, + onShutdownPress + } = props; + + return ( +
+ + + + + + + + + {translate('KeyboardShortcuts')} + + + + + + + {translate('Restart')} + + + + + {translate('Shutdown')} + + + { + formsAuth && +
+ } + + { + formsAuth && + + + Logout + + } + +
+
+ ); +} + +PageHeaderActionsMenu.propTypes = { + formsAuth: PropTypes.bool.isRequired, + onKeyboardShortcutsPress: PropTypes.func.isRequired, + onRestartPress: PropTypes.func.isRequired, + onShutdownPress: PropTypes.func.isRequired +}; + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx deleted file mode 100644 index 6b7da03eb..000000000 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import Icon from 'Components/Icon'; -import Menu from 'Components/Menu/Menu'; -import MenuButton from 'Components/Menu/MenuButton'; -import MenuContent from 'Components/Menu/MenuContent'; -import MenuItem from 'Components/Menu/MenuItem'; -import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; -import { align, icons, kinds } from 'Helpers/Props'; -import { restart, shutdown } from 'Store/Actions/systemActions'; -import translate from 'Utilities/String/translate'; -import styles from './PageHeaderActionsMenu.css'; - -interface PageHeaderActionsMenuProps { - onKeyboardShortcutsPress(): void; -} - -function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) { - const { onKeyboardShortcutsPress } = props; - - const dispatch = useDispatch(); - - const { authentication, isDocker } = useSelector( - (state: AppState) => state.system.status.item - ); - - const formsAuth = authentication === 'forms'; - - const handleRestartPress = useCallback(() => { - dispatch(restart()); - }, [dispatch]); - - const handleShutdownPress = useCallback(() => { - dispatch(shutdown()); - }, [dispatch]); - - return ( -
- - - - - - - - - {translate('KeyboardShortcuts')} - - - {isDocker ? null : ( - <> - - - - - {translate('Restart')} - - - - - {translate('Shutdown')} - - - )} - - {formsAuth ? ( - <> - - - - - {translate('Logout')} - - - ) : null} - - -
- ); -} - -export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js new file mode 100644 index 000000000..3aba95065 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + formsAuth: status.item.authentication === 'forms' + }; + } + ); +} + +const mapDispatchToProps = { + restart, + shutdown +}; + +class PageHeaderActionsMenuConnector extends Component { + + // + // Listeners + + onRestartPress = () => { + this.props.restart(); + }; + + onShutdownPress = () => { + this.props.shutdown(); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +PageHeaderActionsMenuConnector.propTypes = { + restart: PropTypes.func.isRequired, + shutdown: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index c2e368827..aa23f4d88 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import AppUpdatedModal from 'App/AppUpdatedModal'; +import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; import ColorImpairedContext from 'App/ColorImpairedContext'; -import ConnectionLostModal from 'App/ConnectionLostModal'; +import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import SignalRConnector from 'Components/SignalRConnector'; import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import locationShape from 'Helpers/Props/Shapes/locationShape'; @@ -102,12 +102,12 @@ class Page extends Component { {children}
- - diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 5c1f6f42e..5ac032c0f 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { createSelector } from 'reselect'; -import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; +import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchIndexers } from 'Store/Actions/indexerActions'; import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; @@ -54,7 +54,6 @@ const selectIsPopulated = createSelector( (state) => state.indexerStatus.isPopulated, (state) => state.settings.indexerCategories.isPopulated, (state) => state.system.status.isPopulated, - (state) => state.app.translations.isPopulated, ( customFiltersIsPopulated, tagsIsPopulated, @@ -64,8 +63,7 @@ const selectIsPopulated = createSelector( indexersIsPopulated, indexerStatusIsPopulated, indexerCategoriesIsPopulated, - systemStatusIsPopulated, - translationsIsPopulated + systemStatusIsPopulated ) => { return ( customFiltersIsPopulated && @@ -76,8 +74,7 @@ const selectIsPopulated = createSelector( indexersIsPopulated && indexerStatusIsPopulated && indexerCategoriesIsPopulated && - systemStatusIsPopulated && - translationsIsPopulated + systemStatusIsPopulated ); } ); @@ -92,7 +89,6 @@ const selectErrors = createSelector( (state) => state.indexerStatus.error, (state) => state.settings.indexerCategories.error, (state) => state.system.status.error, - (state) => state.app.translations.error, ( customFiltersError, tagsError, @@ -102,8 +98,7 @@ const selectErrors = createSelector( indexersError, indexerStatusError, indexerCategoriesError, - systemStatusError, - translationsError + systemStatusError ) => { const hasError = !!( customFiltersError || @@ -114,8 +109,7 @@ const selectErrors = createSelector( indexersError || indexerStatusError || indexerCategoriesError || - systemStatusError || - translationsError + systemStatusError ); return { @@ -128,8 +122,7 @@ const selectErrors = createSelector( indexersError, indexerStatusError, indexerCategoriesError, - systemStatusError, - translationsError + systemStatusError }; } ); @@ -191,9 +184,6 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchStatus() { dispatch(fetchStatus()); }, - dispatchFetchTranslations() { - dispatch(fetchTranslations()); - }, onResize(dimensions) { dispatch(saveDimensions(dimensions)); }, @@ -227,7 +217,6 @@ class PageConnector extends Component { this.props.dispatchFetchUISettings(); this.props.dispatchFetchGeneralSettings(); this.props.dispatchFetchStatus(); - this.props.dispatchFetchTranslations(); } } @@ -253,7 +242,6 @@ class PageConnector extends Component { dispatchFetchUISettings, dispatchFetchGeneralSettings, dispatchFetchStatus, - dispatchFetchTranslations, ...otherProps } = this.props; @@ -294,7 +282,6 @@ PageConnector.propTypes = { dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchGeneralSettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, - dispatchFetchTranslations: PropTypes.func.isRequired, onSidebarVisibleChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index ce9b0e7e4..75317f113 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -1,19 +1,22 @@ -import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; -import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; +import React, { forwardRef, ReactNode, useCallback } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; import ScrollDirection from 'Helpers/Props/ScrollDirection'; import { isLocked } from 'Utilities/scrollLock'; import styles from './PageContentBody.css'; interface PageContentBodyProps { - className?: string; - innerClassName?: string; + className: string; + innerClassName: string; children: ReactNode; initialScrollTop?: number; - onScroll?: (payload: OnScroll) => void; + onScroll?: (payload) => void; } const PageContentBody = forwardRef( - (props: PageContentBodyProps, ref: ForwardedRef) => { + ( + props: PageContentBodyProps, + ref: React.MutableRefObject + ) => { const { className = styles.contentBody, innerClassName = styles.innerContentBody, @@ -23,7 +26,7 @@ const PageContentBody = forwardRef( } = props; const onScrollWrapper = useCallback( - (payload: OnScroll) => { + (payload) => { if (onScroll && !isLocked()) { onScroll(payload); } diff --git a/frontend/src/Components/Page/PageJumpBar.css b/frontend/src/Components/Page/PageJumpBar.css index f5ae7a729..9a116fb54 100644 --- a/frontend/src/Components/Page/PageJumpBar.css +++ b/frontend/src/Components/Page/PageJumpBar.css @@ -1,5 +1,4 @@ .jumpBar { - z-index: $pageJumpBarZIndex; display: flex; align-content: stretch; align-items: stretch; diff --git a/frontend/src/Components/Page/PageSectionContent.js b/frontend/src/Components/Page/PageSectionContent.js index 2cef9eef1..774b88669 100644 --- a/frontend/src/Components/Page/PageSectionContent.js +++ b/frontend/src/Components/Page/PageSectionContent.js @@ -1,8 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import { kinds } from 'Helpers/Props'; function PageSectionContent(props) { const { @@ -19,7 +17,7 @@ function PageSectionContent(props) { ); } else if (!isFetching && !!error) { return ( - {errorMessage} +
{errorMessage}
); } else if (isPopulated && !error) { return ( diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 6eef54eab..045789075 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 HealthStatus from 'System/Status/Health/HealthStatus'; +import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; @@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); const links = [ { iconName: icons.MOVIE_CONTINUING, - title: () => translate('Indexers'), + title: translate('Indexers'), to: '/', alias: '/indexers', children: [ { - title: () => translate('Stats'), + title: translate('Stats'), to: '/indexers/stats' } ] @@ -33,47 +33,47 @@ const links = [ { iconName: icons.SEARCH, - title: () => translate('Search'), + title: translate('Search'), to: '/search' }, { iconName: icons.ACTIVITY, - title: () => translate('History'), + title: translate('History'), to: '/history' }, { iconName: icons.SETTINGS, - title: () => translate('Settings'), + title: translate('Settings'), to: '/settings', children: [ { - title: () => translate('Indexers'), + title: translate('Indexers'), to: '/settings/indexers' }, { - title: () => translate('Apps'), + title: translate('Apps'), to: '/settings/applications' }, { - title: () => translate('DownloadClients'), + title: translate('DownloadClients'), to: '/settings/downloadclients' }, { - title: () => translate('Connect'), + title: translate('Connect'), to: '/settings/connect' }, { - title: () => translate('Tags'), + title: translate('Tags'), to: '/settings/tags' }, { - title: () => translate('General'), + title: translate('General'), to: '/settings/general' }, { - title: () => translate('UI'), + title: translate('UI'), to: '/settings/ui' } ] @@ -81,32 +81,32 @@ const links = [ { iconName: icons.SYSTEM, - title: () => translate('System'), + title: translate('System'), to: '/system/status', children: [ { - title: () => translate('Status'), + title: translate('Status'), to: '/system/status', - statusComponent: HealthStatus + statusComponent: HealthStatusConnector }, { - title: () => translate('Tasks'), + title: translate('Tasks'), to: '/system/tasks' }, { - title: () => translate('Backup'), + title: translate('Backup'), to: '/system/backup' }, { - title: () => translate('Updates'), + title: translate('Updates'), to: '/system/updates' }, { - title: () => translate('Events'), + title: translate('Events'), to: '/system/events' }, { - title: () => translate('LogFiles'), + title: translate('LogFiles'), to: '/system/logs/files' } ] diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css index 409062f97..5e3e3b52c 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -24,7 +24,6 @@ composes: link; padding: 10px 24px; - padding-left: 35px; } .isActiveLink { @@ -42,6 +41,10 @@ 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 5bf0eb815..77e23c767 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts @@ -8,6 +8,7 @@ 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 8d0e4e790..9ad78db6b 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -63,7 +63,9 @@ class PageSidebarItem extends Component { } - {typeof title === 'function' ? title() : title} + + {title} + { !!StatusComponent && @@ -86,7 +88,7 @@ class PageSidebarItem extends Component { PageSidebarItem.propTypes = { iconName: PropTypes.object, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + title: PropTypes.string.isRequired, to: PropTypes.string.isRequired, isActive: PropTypes.bool, isActiveParent: PropTypes.bool, diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css index e9a1b666d..0b6918296 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -22,14 +22,11 @@ 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 675bdfd02..c93603aa9 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -23,7 +23,6 @@ function PageToolbarButton(props) { isDisabled && styles.isDisabled )} isDisabled={isDisabled || isSpinning} - title={label} {...otherProps} >
@@ -56,9 +56,7 @@ function ProgressBar(props) { styles[kind], enableColorImpairedMode && 'colorImpaired' )} - role="meter" - aria-label={`Progress Bar at ${progress.toFixed(0)}%`} - aria-valuenow={progress.toFixed(0)} + aria-valuenow={progress} aria-valuemin="0" aria-valuemax="100" style={{ width: progressPercent }} @@ -67,7 +65,7 @@ function ProgressBar(props) { { showText ?
void; + onScroll?: (payload) => void; } const Scroller = forwardRef( - (props: ScrollerProps, ref: ForwardedRef) => { + (props: ScrollerProps, ref: React.MutableRefObject) => { const { className, autoFocus = false, @@ -42,7 +30,7 @@ const Scroller = forwardRef( } = props; const internalRef = useRef(); - const currentRef = (ref as MutableRefObject) ?? internalRef; + const currentRef = ref ?? internalRef; useEffect( () => { diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index d39c05e10..3c10c2754 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -54,7 +54,7 @@ function Logger(minimumLogLevel) { } Logger.prototype.cleanse = function(message) { - const apikey = new RegExp(`access_token=${encodeURIComponent(window.Prowlarr.apiKey)}`, 'g'); + const apikey = new RegExp(`access_token=${window.Prowlarr.apiKey}`, 'g'); return message.replace(apikey, 'access_token=(removed)'); }; @@ -98,7 +98,7 @@ class SignalRConnector extends Component { this.connection = new signalR.HubConnectionBuilder() .configureLogging(new Logger(signalR.LogLevel.Information)) - .withUrl(`${url}?access_token=${encodeURIComponent(window.Prowlarr.apiKey)}`) + .withUrl(`${url}?access_token=${window.Prowlarr.apiKey}`) .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (retryContext) => { if (retryContext.elapsedMilliseconds > 180000) { @@ -141,16 +141,6 @@ class SignalRConnector extends Component { console.error(`signalR: Unable to find handler for ${name}`); }; - handleApplications = ({ action, resource }) => { - 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(); @@ -160,8 +150,8 @@ class SignalRConnector extends Component { const resource = body.resource; const status = resource.status; - // Both successful and failed commands need to be - // completed, otherwise they spin until they time out. + // Both sucessful and failed commands need to be + // completed, otherwise they spin until they timeout. if (status === 'completed' || status === 'failed') { this.props.dispatchFinishCommand(resource); @@ -170,16 +160,6 @@ 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(); }; @@ -188,33 +168,14 @@ class SignalRConnector extends Component { this.props.dispatchFetchIndexerStatus(); }; - handleIndexer = ({ action, resource }) => { + handleIndexer = (body) => { + const action = body.action; const section = 'indexers'; - if (action === 'created' || action === 'updated') { - this.props.dispatchUpdateItem({ section, ...resource }); + if (action === 'updated') { + this.props.dispatchUpdateItem({ section, ...body.resource }); } else if (action === 'deleted') { - 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 }); + this.props.dispatchRemoveItem({ section, id: body.resource.id }); } }; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js index 4bf94cf57..207b97752 100644 --- a/frontend/src/Components/Table/Cells/RelativeDateCell.js +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js @@ -1,66 +1,58 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import React, { PureComponent } from 'react'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import TableRowCell from './TableRowCell'; import styles from './RelativeDateCell.css'; -function createRelativeDateCellSelector() { - return createSelector(createUISettingsSelector(), (uiSettings) => { - return { - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - }); -} +class RelativeDateCell extends PureComponent { -function RelativeDateCell(props) { // // Render - const { - className, - date, - includeSeconds, - component: Component, - dispatch, - ...otherProps - } = props; + render() { + const { + className, + date, + includeSeconds, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + component: Component, + dispatch, + ...otherProps + } = this.props; - const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = - useSelector(createRelativeDateCellSelector()); + if (!date) { + return ( + + ); + } - if (!date) { - return ; + return ( + + {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} + + ); } - - return ( - - {getRelativeDate(date, shortDateFormat, showRelativeDates, { - timeFormat, - includeSeconds, - timeForToday: true - })} - - ); } RelativeDateCell.propTypes = { className: PropTypes.string.isRequired, date: PropTypes.string, includeSeconds: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, component: PropTypes.elementType, dispatch: PropTypes.func }; diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js new file mode 100644 index 000000000..ff50d3bc9 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'Components/Link/Link'; +import TableRowCell from './TableRowCell'; +import styles from './TableRowCellButton.css'; + +function TableRowCellButton({ className, ...otherProps }) { + return ( + + ); +} + +TableRowCellButton.propTypes = { + className: PropTypes.string.isRequired +}; + +TableRowCellButton.defaultProps = { + className: styles.cell +}; + +export default TableRowCellButton; diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.tsx b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx deleted file mode 100644 index c80a3d626..000000000 --- a/frontend/src/Components/Table/Cells/TableRowCellButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { ReactNode } from 'react'; -import Link, { LinkProps } from 'Components/Link/Link'; -import TableRowCell from './TableRowCell'; -import styles from './TableRowCellButton.css'; - -interface TableRowCellButtonProps extends LinkProps { - className?: string; - children: ReactNode; -} - -function TableRowCellButton(props: TableRowCellButtonProps) { - const { className = styles.cell, ...otherProps } = props; - - return ( - - ); -} - -export default TableRowCellButton; diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts index 24674c3fc..f9ff7287c 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -1,14 +1,8 @@ -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; + label: string; + columnLabel: string; + isSortable: boolean; isVisible: boolean; isModifiable?: boolean; } diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js index 8afbf9ea0..c41fc982a 100644 --- a/frontend/src/Components/Table/Table.js +++ b/frontend/src/Components/Table/Table.js @@ -107,7 +107,7 @@ function Table(props) { {...getTableHeaderCellProps(otherProps)} {...column} > - {typeof column.label === 'function' ? column.label() : column.label} + {column.label} ); }) @@ -121,7 +121,6 @@ function Table(props) { } Table.propTypes = { - ...TableHeaderCell.props, className: PropTypes.string, horizontalScroll: PropTypes.bool.isRequired, selectAll: PropTypes.bool.isRequired, diff --git a/frontend/src/Components/Table/TableHeaderCell.js b/frontend/src/Components/Table/TableHeaderCell.js index b0ed5c571..21766978b 100644 --- a/frontend/src/Components/Table/TableHeaderCell.js +++ b/frontend/src/Components/Table/TableHeaderCell.js @@ -30,7 +30,6 @@ class TableHeaderCell extends Component { const { className, name, - label, columnLabel, isSortable, isVisible, @@ -54,8 +53,7 @@ class TableHeaderCell extends Component { {...otherProps} component="th" className={className} - label={typeof label === 'function' ? label() : label} - title={typeof columnLabel === 'function' ? columnLabel() : columnLabel} + title={columnLabel} onPress={this.onPress} > {children} @@ -79,8 +77,7 @@ class TableHeaderCell extends Component { TableHeaderCell.propTypes = { className: PropTypes.string, name: PropTypes.string.isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]), - columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + columnLabel: PropTypes.string, isSortable: PropTypes.bool, isVisible: PropTypes.bool, isModifiable: PropTypes.bool, diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js index 402ef5ae1..2d91c7c63 100644 --- a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js @@ -35,7 +35,7 @@ function TableOptionsColumn(props) { isDisabled={isModifiable === false} onChange={onVisibleChange} /> - {typeof label === 'function' ? label() : label} + {label} { @@ -56,7 +56,7 @@ function TableOptionsColumn(props) { TableOptionsColumn.propTypes = { name: PropTypes.string.isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + label: PropTypes.string.isRequired, isVisible: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired, index: PropTypes.number.isRequired, diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js index 77d18463f..100559660 100644 --- a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js @@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
@@ -187,19 +180,16 @@ VirtualTable.propTypes = { className: PropTypes.string.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, scrollIndex: PropTypes.number, - scrollTop: PropTypes.number, scroller: PropTypes.instanceOf(Element).isRequired, focusScroller: PropTypes.bool.isRequired, header: PropTypes.node.isRequired, headerHeight: PropTypes.number.isRequired, - rowRenderer: PropTypes.func.isRequired, - rowHeight: PropTypes.number.isRequired + rowRenderer: PropTypes.func.isRequired }; VirtualTable.defaultProps = { className: styles.tableContainer, headerHeight: 38, - rowHeight: ROW_HEIGHT, focusScroller: true }; diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts deleted file mode 100644 index dfebb2355..000000000 --- a/frontend/src/Components/Table/usePaging.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 fe700b8fe..f4d4e2af4 100644 --- a/frontend/src/Components/TagList.js +++ b/frontend/src/Components/TagList.js @@ -1,15 +1,14 @@ 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((tag) => !!tag) - .sort(sortByProp('label')); + .filter((t) => t !== undefined) + .sort((a, b) => a.label.localeCompare(b.label)); return (
diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js index 8513a65eb..713f2bff4 100644 --- a/frontend/src/Components/keyboardShortcuts.js +++ b/frontend/src/Components/keyboardShortcuts.js @@ -6,51 +6,37 @@ import translate from 'Utilities/String/translate'; export const shortcuts = { OPEN_KEYBOARD_SHORTCUTS_MODAL: { key: '?', - get name() { - return translate('OpenThisModal'); - } + name: translate('OpenThisModal') }, CLOSE_MODAL: { key: 'Esc', - get name() { - return translate('CloseCurrentModal'); - } + name: translate('CloseCurrentModal') }, ACCEPT_CONFIRM_MODAL: { key: 'Enter', - get name() { - return translate('AcceptConfirmationModal'); - } + name: translate('AcceptConfirmationModal') }, MOVIE_SEARCH_INPUT: { key: 's', - get name() { - return translate('FocusSearchBox'); - } + name: translate('FocusSearchBox') }, SAVE_SETTINGS: { key: 'mod+s', - get name() { - return translate('SaveSettings'); - } + name: translate('SaveSettings') }, SCROLL_TOP: { key: 'mod+home', - get name() { - return translate('MovieIndexScrollTop'); - } + name: translate('MovieIndexScrollTop') }, SCROLL_BOTTOM: { key: 'mod+end', - get name() { - return translate('MovieIndexScrollBottom'); - } + name: translate('MovieIndexScrollBottom') } }; @@ -81,10 +67,8 @@ function keyboardShortcuts(WrappedComponent) { }; unbindShortcut = (key) => { - if (this._mousetrap != null) { - delete this._mousetrapBindings[key]; - this._mousetrap.unbind(key); - } + delete this._mousetrapBindings[key]; + this._mousetrap.unbind(key); }; unbindAllShortcuts = () => { diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx index f688a6253..ec13c6ab8 100644 --- a/frontend/src/Components/withScrollPosition.tsx +++ b/frontend/src/Components/withScrollPosition.tsx @@ -1,30 +1,24 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; import scrollPositions from 'Store/scrollPositions'; -interface WrappedComponentProps { - initialScrollTop: number; -} - -interface ScrollPositionProps { - history: RouteComponentProps['history']; - location: RouteComponentProps['location']; - match: RouteComponentProps['match']; -} - -function withScrollPosition( - WrappedComponent: React.FC, - scrollPositionKey: string -) { - function ScrollPosition(props: ScrollPositionProps) { +function withScrollPosition(WrappedComponent, scrollPositionKey) { + function ScrollPosition(props) { const { history } = props; const initialScrollTop = - history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0; + history.action === 'POP' || + (history.location.state && history.location.state.restoreScrollPosition) + ? scrollPositions[scrollPositionKey] + : 0; return ; } + ScrollPosition.propTypes = { + history: PropTypes.object.isRequired, + }; + return ScrollPosition; } diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css index e0f1bf5dc..bf31501dd 100644 --- a/frontend/src/Content/Fonts/fonts.css +++ b/frontend/src/Content/Fonts/fonts.css @@ -25,3 +25,14 @@ 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 new file mode 100644 index 000000000..86038dba8 Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.ttf differ diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff new file mode 100644 index 000000000..bc4cc324b Binary files /dev/null and b/frontend/src/Content/Fonts/text-security-disc.woff differ diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json index f53279dd3..d14732f60 100644 --- a/frontend/src/Content/Images/Icons/manifest.json +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -1,19 +1,18 @@ { - "name": "Prowlarr", + "name": "", "icons": [ { - "src": "android-chrome-192x192.png", + "src": "/Content/Images/Icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "android-chrome-512x512.png", + "src": "/Content/Images/Icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], - "start_url": "../../../../", "theme_color": "#3a3f51", "background_color": "#3a3f51", "display": "standalone" -} +} \ No newline at end of file diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts deleted file mode 100644 index 417db8178..000000000 --- a/frontend/src/DownloadClient/DownloadProtocol.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DownloadProtocol = 'usenet' | 'torrent' | 'unknown'; - -export default DownloadProtocol; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js index 17a04e403..920c59a31 100644 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js @@ -11,7 +11,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 { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; +import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings'; import translate from 'Utilities/String/translate'; import styles from './AuthenticationRequiredModalContent.css'; @@ -34,8 +34,7 @@ function AuthenticationRequiredModalContent(props) { authenticationMethod, authenticationRequired, username, - password, - passwordConfirmation + password } = settings; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; @@ -64,75 +63,71 @@ function AuthenticationRequiredModalContent(props) { className={styles.authRequiredAlert} kind={kinds.WARNING} > - {translate('AuthenticationRequiredWarning')} + {authenticationRequiredWarning} { isPopulated && !error ?
- {translate('AuthenticationMethod')} + {translate('Authentication')} - - {translate('AuthenticationRequired')} + { + authenticationEnabled ? + + {translate('AuthenticationRequired')} - - + + : + null + } - - {translate('Username')} + { + authenticationEnabled ? + + {translate('Username')} - - + + : + null + } - - {translate('Password')} + { + authenticationEnabled ? + + {translate('Password')} - - - - - {translate('PasswordConfirmation')} - - - + + : + null + }
: null } diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts deleted file mode 100644 index 3caf66df2..000000000 --- a/frontend/src/Helpers/Hooks/useCurrentPage.ts +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 24cffb2f1..000000000 --- a/frontend/src/Helpers/Hooks/useModalOpenState.ts +++ /dev/null @@ -1,17 +0,0 @@ -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/Hooks/usePrevious.tsx b/frontend/src/Helpers/Hooks/usePrevious.tsx deleted file mode 100644 index b594e2632..000000000 --- a/frontend/src/Helpers/Hooks/usePrevious.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export default function usePrevious(value: T): T | undefined { - const ref = useRef(); - - useEffect(() => { - ref.current = value; - }, [value]); - - return ref.current; -} diff --git a/frontend/src/Helpers/Hooks/useSelectState.tsx b/frontend/src/Helpers/Hooks/useSelectState.tsx deleted file mode 100644 index 8fb96e42a..000000000 --- a/frontend/src/Helpers/Hooks/useSelectState.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { cloneDeep } from 'lodash'; -import { useReducer } from 'react'; -import ModelBase from 'App/ModelBase'; -import areAllSelected from 'Utilities/Table/areAllSelected'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; - -export type SelectedState = Record; - -export interface SelectState { - selectedState: SelectedState; - lastToggled: number | null; - allSelected: boolean; - allUnselected: boolean; -} - -export type SelectAction = - | { type: 'reset' } - | { type: 'selectAll'; items: ModelBase[] } - | { type: 'unselectAll'; items: ModelBase[] } - | { - type: 'toggleSelected'; - id: number; - isSelected: boolean; - shiftKey: boolean; - items: ModelBase[]; - } - | { - type: 'removeItem'; - id: number; - } - | { - type: 'updateItems'; - items: ModelBase[]; - }; - -export type Dispatch = (action: SelectAction) => void; - -const initialState = { - selectedState: {}, - lastToggled: null, - allSelected: false, - allUnselected: true, - items: [], -}; - -function getSelectedState(items: ModelBase[], existingState: SelectedState) { - return items.reduce((acc: SelectedState, item) => { - const id = item.id; - - acc[id] = existingState[id] ?? false; - - return acc; - }, {}); -} - -function selectReducer(state: SelectState, action: SelectAction): SelectState { - const { selectedState } = state; - - switch (action.type) { - case 'reset': { - return cloneDeep(initialState); - } - case 'selectAll': { - return { - ...selectAll(selectedState, true), - }; - } - case 'unselectAll': { - return { - ...selectAll(selectedState, false), - }; - } - case 'toggleSelected': { - const result = { - ...toggleSelected( - state, - action.items, - action.id, - action.isSelected, - action.shiftKey - ), - }; - - return result; - } - case 'updateItems': { - const nextSelectedState = getSelectedState(action.items, selectedState); - - return { - ...state, - ...areAllSelected(nextSelectedState), - selectedState: nextSelectedState, - }; - } - default: { - throw new Error(`Unhandled action type: ${action.type}`); - } - } -} - -export default function useSelectState(): [SelectState, Dispatch] { - const selectedState = getSelectedState([], {}); - - const [state, dispatch] = useReducer(selectReducer, { - selectedState, - lastToggled: null, - allSelected: false, - allUnselected: true, - }); - - return [state, dispatch]; -} diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts deleted file mode 100644 index 885c73470..000000000 --- a/frontend/src/Helpers/Props/TooltipPosition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; - -export default TooltipPosition; diff --git a/frontend/src/Helpers/Props/align.ts b/frontend/src/Helpers/Props/align.js similarity index 100% rename from frontend/src/Helpers/Props/align.ts rename to frontend/src/Helpers/Props/align.js diff --git a/frontend/src/Helpers/Props/filterBuilderTypes.js b/frontend/src/Helpers/Props/filterBuilderTypes.js index c0806fabc..776ba2afc 100644 --- a/frontend/src/Helpers/Props/filterBuilderTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderTypes.js @@ -1,18 +1,14 @@ import * as filterTypes from './filterTypes'; export const ARRAY = 'array'; -export const CONTAINS = 'contains'; export const DATE = 'date'; -export const EQUAL = 'equal'; export const EXACT = 'exact'; export const NUMBER = 'number'; export const STRING = 'string'; export const all = [ ARRAY, - CONTAINS, DATE, - EQUAL, EXACT, NUMBER, STRING @@ -24,10 +20,6 @@ export const possibleFilterTypes = { { key: filterTypes.NOT_CONTAINS, value: 'does not contain' } ], - [CONTAINS]: [ - { key: filterTypes.CONTAINS, value: 'contains' } - ], - [DATE]: [ { key: filterTypes.LESS_THAN, value: 'is before' }, { key: filterTypes.GREATER_THAN, value: 'is after' }, @@ -37,10 +29,6 @@ export const possibleFilterTypes = { { key: filterTypes.NOT_IN_NEXT, value: 'not in the next' } ], - [EQUAL]: [ - { key: filterTypes.EQUAL, value: 'is' } - ], - [EXACT]: [ { key: filterTypes.EQUAL, value: 'is' }, { key: filterTypes.NOT_EQUAL, value: 'is not' } @@ -59,10 +47,6 @@ export const possibleFilterTypes = { { key: filterTypes.CONTAINS, value: 'contains' }, { key: filterTypes.NOT_CONTAINS, value: 'does not contain' }, { key: filterTypes.EQUAL, value: 'equal' }, - { key: filterTypes.NOT_EQUAL, value: 'not equal' }, - { key: filterTypes.STARTS_WITH, value: 'starts with' }, - { key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' }, - { key: filterTypes.ENDS_WITH, value: 'ends with' }, - { key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' } + { key: filterTypes.NOT_EQUAL, value: 'not equal' } ] }; diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 73ef41956..7fed535f2 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -2,10 +2,9 @@ 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 CATEGORY = 'category'; +export const MOVIE_STATUS = 'movieStatus'; export const TAG = 'tag'; diff --git a/frontend/src/Helpers/Props/filterTypePredicates.js b/frontend/src/Helpers/Props/filterTypePredicates.js index d07059c02..a3ea11956 100644 --- a/frontend/src/Helpers/Props/filterTypePredicates.js +++ b/frontend/src/Helpers/Props/filterTypePredicates.js @@ -39,22 +39,6 @@ const filterTypePredicates = { [filterTypes.NOT_EQUAL]: function(itemValue, filterValue) { return itemValue !== filterValue; - }, - - [filterTypes.STARTS_WITH]: function(itemValue, filterValue) { - return itemValue.toLowerCase().startsWith(filterValue.toLowerCase()); - }, - - [filterTypes.NOT_STARTS_WITH]: function(itemValue, filterValue) { - return !itemValue.toLowerCase().startsWith(filterValue.toLowerCase()); - }, - - [filterTypes.ENDS_WITH]: function(itemValue, filterValue) { - return itemValue.toLowerCase().endsWith(filterValue.toLowerCase()); - }, - - [filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) { - return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase()); } }; diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js index 239a4e7e9..993e8df57 100644 --- a/frontend/src/Helpers/Props/filterTypes.js +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -10,10 +10,6 @@ export const LESS_THAN = 'lessThan'; export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual'; export const NOT_CONTAINS = 'notContains'; export const NOT_EQUAL = 'notEqual'; -export const STARTS_WITH = 'startsWith'; -export const NOT_STARTS_WITH = 'notStartsWith'; -export const ENDS_WITH = 'endsWith'; -export const NOT_ENDS_WITH = 'notEndsWith'; export const all = [ CONTAINS, @@ -27,9 +23,5 @@ export const all = [ IN_LAST, NOT_IN_LAST, IN_NEXT, - NOT_IN_NEXT, - STARTS_WITH, - NOT_STARTS_WITH, - ENDS_WITH, - NOT_ENDS_WITH + NOT_IN_NEXT ]; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 773748996..00e2c1aa0 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -43,7 +43,6 @@ import { faChevronCircleRight as fasChevronCircleRight, faChevronCircleUp as fasChevronCircleUp, faCircle as fasCircle, - faCircleDown as fasCircleDown, faCloud as fasCloud, faCloudDownloadAlt as fasCloudDownloadAlt, faCog as fasCog, @@ -73,10 +72,8 @@ import { faLanguage as fasLanguage, faLaptop as fasLaptop, faLevelUpAlt as fasLevelUpAlt, - faListCheck as fasListCheck, faLocationArrow as fasLocationArrow, faLock as fasLock, - faMagnet as fasMagnet, faMedkit as fasMedkit, faMinus as fasMinus, faMusic as fasMusic, @@ -142,7 +139,6 @@ 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; @@ -184,8 +180,6 @@ export const INTERACTIVE = fasUser; export const KEYBOARD = farKeyboard; export const LOCK = fasLock; export const LOGOUT = fasSignOutAlt; -export const MAGNET = fasMagnet; -export const MANAGE = fasListCheck; export const MEDIA_INFO = farFileInvoice; export const MISSING = fasExclamationTriangle; export const MONITORED = fasBookmark; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index f9cd58e6d..7a11bb0c7 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -1,5 +1,6 @@ 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'; @@ -8,8 +9,6 @@ export const KEY_VALUE_LIST = 'keyValueList'; export const INFO = 'info'; export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; export const CATEGORY_SELECT = 'newznabCategorySelect'; -export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; -export const FLOAT = 'float'; export const NUMBER = 'number'; export const OAUTH = 'oauth'; export const PASSWORD = 'password'; @@ -26,6 +25,7 @@ export const TAG_SELECT = 'tagSelect'; export const all = [ AUTO_COMPLETE, APP_PROFILE_SELECT, + AVAILABILITY_SELECT, CAPTCHA, CARDIGANNCAPTCHA, CHECK, @@ -34,7 +34,6 @@ export const all = [ INFO, MOVIE_MONITORED_SELECT, CATEGORY_SELECT, - FLOAT, NUMBER, OAUTH, PASSWORD, diff --git a/frontend/src/Helpers/Props/kinds.ts b/frontend/src/Helpers/Props/kinds.js similarity index 72% rename from frontend/src/Helpers/Props/kinds.ts rename to frontend/src/Helpers/Props/kinds.js index 7ce606716..b0f5ac87f 100644 --- a/frontend/src/Helpers/Props/kinds.ts +++ b/frontend/src/Helpers/Props/kinds.js @@ -7,6 +7,7 @@ export const PRIMARY = 'primary'; export const PURPLE = 'purple'; export const SUCCESS = 'success'; export const WARNING = 'warning'; +export const QUEUE = 'queue'; export const all = [ DANGER, @@ -18,15 +19,5 @@ export const all = [ PURPLE, SUCCESS, WARNING, -] as const; - -export type Kind = - | 'danger' - | 'default' - | 'disabled' - | 'info' - | 'inverse' - | 'primary' - | 'purple' - | 'success' - | 'warning'; + QUEUE +]; diff --git a/frontend/src/Helpers/Props/sizes.ts b/frontend/src/Helpers/Props/sizes.js similarity index 71% rename from frontend/src/Helpers/Props/sizes.ts rename to frontend/src/Helpers/Props/sizes.js index ca7a50fbf..d7f85df5e 100644 --- a/frontend/src/Helpers/Props/sizes.ts +++ b/frontend/src/Helpers/Props/sizes.js @@ -4,6 +4,4 @@ export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const; - -export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge'; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js index 6d5ab260e..63543f040 100644 --- a/frontend/src/History/Details/HistoryDetails.js +++ b/frontend/src/History/Details/HistoryDetails.js @@ -3,7 +3,6 @@ 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'; @@ -11,10 +10,7 @@ function HistoryDetails(props) { const { indexer, eventType, - date, - data, - shortDateFormat, - timeFormat + data } = props; if (eventType === 'indexerQuery' || eventType === 'indexerRss') { @@ -22,13 +18,8 @@ function HistoryDetails(props) { query, queryResults, categories, - limit, - offset, source, - host, - url, - elapsedTime, - cached + url } = data; return ( @@ -40,93 +31,43 @@ function HistoryDetails(props) { /> { - indexer ? + !!indexer && : - null + /> } { - data ? + !!data && : - null + /> } { - data ? + !!data && : - null + /> } { - limit ? - : - null - } - - { - offset ? - : - null - } - - { - data ? + !!data && : - null + /> } { - data ? - : - null - } - - { - data ? + !!data && {translate('Link')} : '-'} - /> : - null - } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null + /> } ); @@ -135,156 +76,59 @@ function HistoryDetails(props) { if (eventType === 'releaseGrabbed') { const { source, - host, - grabTitle, - url, - publishedDate, - infoUrl, - downloadClient, - downloadClientName, - elapsedTime, - grabMethod + title, + url } = data; - const downloadClientNameInfo = downloadClientName ?? downloadClient; - return ( { - indexer ? + !!indexer && : - null + /> } { - data ? + !!data && : - null + /> } { - data ? + !!data && : - null + title={translate('Title')} + data={title ? title : '-'} + /> } { - data ? - : - null - } - - { - infoUrl ? - {infoUrl}} - /> : - null - } - - { - publishedDate ? - : - null - } - - { - downloadClientNameInfo ? - : - null - } - - { - data ? + !!data && {translate('Link')} : '-'} - /> : - null - } - - { - elapsedTime ? - : - null - } - - { - grabMethod ? - : - null - } - - { - date ? - : - null + /> } ); } if (eventType === 'indexerAuth') { - const { elapsedTime } = data; - return ( { - indexer ? + !!indexer && : - null - } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null + /> } ); @@ -297,15 +141,6 @@ function HistoryDetails(props) { title={translate('Name')} data={data.query} /> - - { - date ? - : - null - } ); } @@ -313,7 +148,6 @@ 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.css b/frontend/src/History/Details/HistoryDetailsModal.css new file mode 100644 index 000000000..271d422ff --- /dev/null +++ b/frontend/src/History/Details/HistoryDetailsModal.css @@ -0,0 +1,5 @@ +.markAsFailedButton { + composes: button from '~Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Components/Form/InfoInput.css.d.ts b/frontend/src/History/Details/HistoryDetailsModal.css.d.ts similarity index 83% rename from frontend/src/Components/Form/InfoInput.css.d.ts rename to frontend/src/History/Details/HistoryDetailsModal.css.d.ts index 65c237dff..a8cc499e2 100644 --- a/frontend/src/Components/Form/InfoInput.css.d.ts +++ b/frontend/src/History/Details/HistoryDetailsModal.css.d.ts @@ -1,7 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'message': string; + 'markAsFailedButton': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js index 560955de3..e6f960c48 100644 --- a/frontend/src/History/Details/HistoryDetailsModal.js +++ b/frontend/src/History/Details/HistoryDetailsModal.js @@ -1,13 +1,16 @@ import PropTypes from 'prop-types'; import React from 'react'; import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; import Modal from 'Components/Modal/Modal'; 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 translate from 'Utilities/String/translate'; import HistoryDetails from './HistoryDetails'; +import styles from './HistoryDetailsModal.css'; function getHeaderTitle(eventType) { switch (eventType) { @@ -29,10 +32,11 @@ function HistoryDetailsModal(props) { isOpen, eventType, indexer, - date, data, + isMarkingAsFailed, shortDateFormat, timeFormat, + onMarkAsFailedPress, onModalClose } = props; @@ -50,7 +54,6 @@ function HistoryDetailsModal(props) { + { + eventType === 'grabbed' && + + Mark as Failed + + } + + + + ); + } +} + +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 deleted file mode 100644 index be1413769..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx +++ /dev/null @@ -1,434 +0,0 @@ -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 new file mode 100644 index 000000000..0dc810608 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js @@ -0,0 +1,83 @@ +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 createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import AddIndexerModalContent from './AddIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector('indexers.schema'), + (indexers) => { + const { + isFetching, + isPopulated, + error, + items, + sortDirection, + sortKey + } = indexers; + + return { + isFetching, + isPopulated, + error, + indexers: items, + sortKey, + sortDirection + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerSchema, + selectIndexerSchema, + setIndexerSchemaSort +}; + +class AddIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerSchema(); + } + + // + // Listeners + + onIndexerSelect = ({ implementation, name }) => { + this.props.selectIndexerSchema({ implementation, 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/AddIndexerPresetMenuItem.js b/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js index 8f98d0e12..03196e526 100644 --- a/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js +++ b/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js @@ -10,14 +10,12 @@ class AddIndexerPresetMenuItem extends Component { onPress = () => { const { name, - implementation, - implementationName + implementation } = this.props; this.props.onPress({ name, - implementation, - implementationName + implementation }); }; @@ -28,7 +26,6 @@ class AddIndexerPresetMenuItem extends Component { const { name, implementation, - implementationName, ...otherProps } = this.props; @@ -46,7 +43,6 @@ class AddIndexerPresetMenuItem extends Component { AddIndexerPresetMenuItem.propTypes = { name: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.js b/frontend/src/Indexer/Add/SelectIndexerRow.js new file mode 100644 index 000000000..c3f33220d --- /dev/null +++ b/frontend/src/Indexer/Add/SelectIndexerRow.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel'; +import firstCharToUpper from 'Utilities/String/firstCharToUpper'; +import translate from 'Utilities/String/translate'; +import styles from './SelectIndexerRow.css'; + +class SelectIndexerRow extends Component { + + // + // Listeners + + onPress = () => { + const { + implementation, + name + } = this.props; + + this.props.onIndexerSelect({ implementation, name }); + }; + + // + // Render + + render() { + const { + protocol, + privacy, + name, + language, + description, + isExistingIndexer + } = this.props; + + return ( + + + + + + + {name} + { + isExistingIndexer ? + : + null + } + + + + {language} + + + + {description} + + + + {translate(firstCharToUpper(privacy))} + + + ); + } +} + +SelectIndexerRow.propTypes = { + name: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + privacy: PropTypes.string.isRequired, + language: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onIndexerSelect: PropTypes.func.isRequired, + isExistingIndexer: PropTypes.bool.isRequired +}; + +export default SelectIndexerRow; diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx deleted file mode 100644 index 157050e41..000000000 --- a/frontend/src/Indexer/Add/SelectIndexerRow.tsx +++ /dev/null @@ -1,78 +0,0 @@ -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, IndexerPrivacy } from 'Indexer/Indexer'; -import translate from 'Utilities/String/translate'; -import styles from './SelectIndexerRow.css'; - -interface SelectIndexerRowProps { - name: string; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; - language: string; - description: string; - capabilities: IndexerCapabilities; - implementation: string; - implementationName: string; - isExistingIndexer: boolean; - onIndexerSelect(...args: unknown[]): void; -} - -function SelectIndexerRow(props: SelectIndexerRowProps) { - const { - name, - protocol, - privacy, - language, - description, - capabilities, - implementation, - implementationName, - isExistingIndexer, - onIndexerSelect, - } = props; - - const onPress = useCallback(() => { - onIndexerSelect({ implementation, implementationName, name }); - }, [implementation, implementationName, name, onIndexerSelect]); - - return ( - - - - - - - {name} - {isExistingIndexer ? ( - - ) : null} - - - {language} - - {description} - - - - - - - - - - ); -} - -export default SelectIndexerRow; diff --git a/frontend/src/Indexer/Add/SelectIndexerRowConnector.js b/frontend/src/Indexer/Add/SelectIndexerRowConnector.js new file mode 100644 index 000000000..f507689c8 --- /dev/null +++ b/frontend/src/Indexer/Add/SelectIndexerRowConnector.js @@ -0,0 +1,18 @@ + +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector'; +import SelectIndexerRow from './SelectIndexerRow'; + +function createMapStateToProps() { + return createSelector( + createExistingIndexerSelector(), + (isExistingIndexer, dimensions) => { + return { + isExistingIndexer + }; + } + ); +} + +export default connect(createMapStateToProps)(SelectIndexerRow); diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModal.js b/frontend/src/Indexer/Delete/DeleteIndexerModal.js new file mode 100644 index 000000000..aed954829 --- /dev/null +++ b/frontend/src/Indexer/Delete/DeleteIndexerModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector'; + +function DeleteIndexerModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteIndexerModal.propTypes = { + ...DeleteIndexerModalContentConnector.propTypes, + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteIndexerModal; diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx b/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx deleted file mode 100644 index 13850aa77..000000000 --- a/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import DeleteIndexerModalContent from './DeleteIndexerModalContent'; - -interface DeleteIndexerModalProps { - isOpen: boolean; - indexerId: number; - onModalClose(): void; -} - -function DeleteIndexerModal(props: DeleteIndexerModalProps) { - const { isOpen, indexerId, onModalClose } = props; - - return ( - - - - ); -} - -export default DeleteIndexerModal; diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js new file mode 100644 index 000000000..e3d46e108 --- /dev/null +++ b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +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 translate from 'Utilities/String/translate'; + +class DeleteIndexerModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false, + addImportExclusion: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + }; + + onAddImportExclusionChange = ({ value }) => { + this.setState({ addImportExclusion: value }); + }; + + onDeleteMovieConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + const addImportExclusion = this.state.addImportExclusion; + + this.setState({ deleteFiles: false, addImportExclusion: false }); + this.props.onDeletePress(deleteFiles, addImportExclusion); + }; + + // + // Render + + render() { + const { + name, + onModalClose + } = this.props; + + return ( + + + Delete - {name} + + + + {`Are you sure you want to delete ${name} from Prowlarr`} + + + + + + + + + ); + } +} + +DeleteIndexerModalContent.propTypes = { + name: PropTypes.string.isRequired, + onDeletePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteIndexerModalContent; diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx deleted file mode 100644 index aeae273a9..000000000 --- a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Button from 'Components/Link/Button'; -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 Indexer from 'Indexer/Indexer'; -import { deleteIndexer } from 'Store/Actions/indexerActions'; -import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector'; -import translate from 'Utilities/String/translate'; - -interface DeleteIndexerModalContentProps { - indexerId: number; - onModalClose(): void; -} - -function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) { - const { indexerId, onModalClose } = props; - - const { name } = useSelector( - createIndexerSelectorForHook(indexerId) - ) as Indexer; - const dispatch = useDispatch(); - - const onConfirmDelete = useCallback(() => { - dispatch(deleteIndexer({ id: indexerId })); - - onModalClose(); - }, [indexerId, dispatch, onModalClose]); - - return ( - - - {translate('Delete')} - {name} - - - - {translate('AreYouSureYouWantToDeleteIndexer', { name })} - - - - - - - - - ); -} - -export default DeleteIndexerModalContent; diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js b/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js new file mode 100644 index 000000000..1e92eb845 --- /dev/null +++ b/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js @@ -0,0 +1,57 @@ +import { push } from 'connected-react-router'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { deleteIndexer } from 'Store/Actions/indexerActions'; +import createIndexerSelector from 'Store/Selectors/createIndexerSelector'; +import DeleteIndexerModalContent from './DeleteIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + createIndexerSelector(), + (indexer) => { + return indexer; + } + ); +} + +const mapDispatchToProps = { + deleteIndexer, + push +}; + +class DeleteIndexerModalContentConnector extends Component { + + // + // Listeners + + onDeletePress = () => { + this.props.deleteIndexer({ + id: this.props.indexerId + }); + + this.props.onModalClose(true); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +DeleteIndexerModalContentConnector.propTypes = { + indexerId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired, + deleteIndexer: PropTypes.func.isRequired, + push: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector); diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js index 7dabc50d9..b83522fcf 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js @@ -26,8 +26,6 @@ function EditIndexerModalContent(props) { isTesting, saveError, item, - hasUsenetDownloadClients, - hasTorrentDownloadClients, onInputChange, onFieldChange, onModalClose, @@ -50,18 +48,15 @@ function EditIndexerModalContent(props) { appProfileId, tags, fields, - priority, - protocol, - downloadClientId + priority } = item; const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`; - const showDownloadClientInput = downloadClientId.value > 0 || protocol.value === 'usenet' && hasUsenetDownloadClients || protocol.value === 'torrent' && hasTorrentDownloadClients; return ( - {id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })} + {`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`} @@ -97,7 +92,7 @@ function EditIndexerModalContent(props) { @@ -144,7 +139,6 @@ function EditIndexerModalContent(props) { }) : null } - - {showDownloadClientInput ? - - {translate('DownloadClient')} - - - : null - } - {translate('Tags')} @@ -188,7 +163,6 @@ function EditIndexerModalContent(props) { type={inputTypes.TAG} name="tags" helpText={translate('IndexerTagsHelpText')} - helpTextWarning={translate('IndexerTagsHelpTextWarning')} {...tags} onChange={onInputChange} /> @@ -248,8 +222,6 @@ EditIndexerModalContent.propTypes = { isTesting: PropTypes.bool.isRequired, saveError: PropTypes.object, item: PropTypes.object.isRequired, - hasUsenetDownloadClients: PropTypes.bool.isRequired, - hasTorrentDownloadClients: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, onFieldChange: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContentConnector.js b/frontend/src/Indexer/Edit/EditIndexerModalContentConnector.js index 66fdbbc15..c76dd5ce4 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContentConnector.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContentConnector.js @@ -3,23 +3,17 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions'; -import { fetchDownloadClients, toggleAdvancedSettings } from 'Store/Actions/settingsActions'; +import { toggleAdvancedSettings } from 'Store/Actions/settingsActions'; import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector'; import EditIndexerModalContent from './EditIndexerModalContent'; function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - (state) => state.settings.downloadClients, createIndexerSchemaSelector(), - (advancedSettings, downloadClients, indexer) => { - const usenetDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'usenet'); - const torrentDownloadClients = downloadClients.items.filter((downloadClient) => downloadClient.protocol === 'torrent'); - + (advancedSettings, indexer) => { return { advancedSettings, - hasUsenetDownloadClients: usenetDownloadClients.length > 0, - hasTorrentDownloadClients: torrentDownloadClients.length > 0, ...indexer }; } @@ -31,8 +25,7 @@ const mapDispatchToProps = { setIndexerFieldValue, saveIndexer, testIndexer, - toggleAdvancedSettings, - dispatchFetchDownloadClients: fetchDownloadClients + toggleAdvancedSettings }; class EditIndexerModalContentConnector extends Component { @@ -40,10 +33,6 @@ class EditIndexerModalContentConnector extends Component { // // Lifecycle - componentDidMount() { - this.props.dispatchFetchDownloadClients(); - } - componentDidUpdate(prevProps, prevState) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { this.props.onModalClose(); @@ -101,8 +90,7 @@ EditIndexerModalContentConnector.propTypes = { toggleAdvancedSettings: PropTypes.func.isRequired, saveIndexer: PropTypes.func.isRequired, testIndexer: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired + onModalClose: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector); diff --git a/frontend/src/Indexer/Index/IndexerIndex.tsx b/frontend/src/Indexer/Index/IndexerIndex.tsx index e20e269f8..df712a1fc 100644 --- a/frontend/src/Indexer/Index/IndexerIndex.tsx +++ b/frontend/src/Indexer/Index/IndexerIndex.tsx @@ -1,16 +1,6 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { SelectProvider } from 'App/SelectContext'; -import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; -import IndexerAppState, { - IndexerIndexAppState, -} from 'App/State/IndexerAppState'; import { APP_INDEXER_SYNC } from 'Commands/commandNames'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; @@ -28,17 +18,12 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; import NoIndexer from 'Indexer/NoIndexer'; import { executeCommand } from 'Store/Actions/commandActions'; -import { - cloneIndexer, - fetchIndexers, - testAllIndexers, -} from 'Store/Actions/indexerActions'; +import { testAllIndexers } from 'Store/Actions/indexerActions'; import { setIndexerFilter, setIndexerSort, setIndexerTableOption, } from 'Store/Actions/indexerIndexActions'; -import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import scrollPositions from 'Store/scrollPositions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -56,7 +41,9 @@ import IndexerIndexTable from './Table/IndexerIndexTable'; import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions'; import styles from './IndexerIndex.css'; -const getViewComponent = () => IndexerIndexTable; +function getViewComponent() { + return IndexerIndexTable; +} interface IndexerIndexProps { initialScrollTop?: number; @@ -77,25 +64,27 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { sortKey, sortDirection, view, - }: IndexerAppState & IndexerIndexAppState & ClientSideCollectionAppState = - useSelector(createIndexerClientSideCollectionItemsSelector('indexerIndex')); + } = useSelector( + createIndexerClientSideCollectionItemsSelector('indexerIndex') + ); const isSyncingIndexers = useSelector( createCommandExecutingSelector(APP_INDEXER_SYNC) ); const { isSmallScreen } = useSelector(createDimensionsSelector()); const dispatch = useDispatch(); - const scrollerRef = useRef(null); + const scrollerRef = useRef(); const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); - const [jumpToCharacter, setJumpToCharacter] = useState( - undefined - ); + const [jumpToCharacter, setJumpToCharacter] = useState(null); const [isSelectMode, setIsSelectMode] = useState(false); - useEffect(() => { - dispatch(fetchIndexers()); - dispatch(fetchIndexerStatus()); + const onAppIndexerSyncPress = useCallback(() => { + dispatch( + executeCommand({ + name: APP_INDEXER_SYNC, + }) + ); }, [dispatch]); const onAddIndexerPress = useCallback(() => { @@ -114,24 +103,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { setIsEditIndexerModalOpen(false); }, [setIsEditIndexerModalOpen]); - const onCloneIndexerPress = useCallback( - (id: number) => { - dispatch(cloneIndexer({ id })); - - setIsEditIndexerModalOpen(true); - }, - [dispatch, setIsEditIndexerModalOpen] - ); - - const onAppIndexerSyncPress = useCallback(() => { - dispatch( - executeCommand({ - name: APP_INDEXER_SYNC, - forceSync: true, - }) - ); - }, [dispatch]); - const onTestAllPress = useCallback(() => { dispatch(testAllIndexers()); }, [dispatch]); @@ -141,53 +112,53 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { }, [isSelectMode, setIsSelectMode]); const onTableOptionChange = useCallback( - (payload: unknown) => { + (payload) => { dispatch(setIndexerTableOption(payload)); }, [dispatch] ); const onSortSelect = useCallback( - (value: string) => { + (value) => { dispatch(setIndexerSort({ sortKey: value })); }, [dispatch] ); const onFilterSelect = useCallback( - (value: string) => { + (value) => { dispatch(setIndexerFilter({ selectedFilterKey: value })); }, [dispatch] ); const onJumpBarItemPress = useCallback( - (character: string) => { + (character) => { setJumpToCharacter(character); }, [setJumpToCharacter] ); const onScroll = useCallback( - ({ scrollTop }: { scrollTop: number }) => { - setJumpToCharacter(undefined); - scrollPositions.indexerIndex = scrollTop; + ({ scrollTop }) => { + setJumpToCharacter(null); + scrollPositions.seriesIndex = scrollTop; }, [setJumpToCharacter] ); const jumpBarItems = useMemo(() => { - // Reset if not sorting by sortName - if (sortKey !== 'sortName') { + // Reset if not sorting by sortTitle + if (sortKey !== 'sortTitle') { return { order: [], }; } - const characters = items.reduce((acc: Record, item) => { - let char = item.sortName.charAt(0); + const characters = items.reduce((acc, item) => { + let char = item.sortTitle.charAt(0); - if (!isNaN(Number(char))) { + if (!isNaN(char)) { char = '#'; } @@ -219,7 +190,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { return ( - + { { /> @@ -278,10 +245,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { optionsComponent={IndexerIndexTableOptions} onTableOptionChange={onTableOptionChange} > - + @@ -306,17 +270,13 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { {isFetching && !isPopulated ? : null} - {!isFetching && !!error ? ( -
{translate('UnableToLoadIndexers')}
- ) : null} + {!isFetching && !!error ?
Unable to load indexers
: null} {isLoaded ? (
@@ -328,7 +288,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { jumpToCharacter={jumpToCharacter} isSelectMode={isSelectMode} isSmallScreen={isSmallScreen} - onCloneIndexerPress={onCloneIndexerPress} /> @@ -336,10 +295,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { ) : null} {!error && isPopulated && !items.length ? ( - + ) : null} {isLoaded && !!jumpBarItems.order.length ? ( diff --git a/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx b/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx index 1b4bfb6de..8a151907a 100644 --- a/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx +++ b/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx @@ -1,13 +1,12 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import FilterModal from 'Components/Filter/FilterModal'; import { setIndexerFilter } from 'Store/Actions/indexerIndexActions'; function createIndexerSelector() { return createSelector( - (state: AppState) => state.indexers.items, + (state) => state.indexers.items, (indexers) => { return indexers; } @@ -16,20 +15,14 @@ function createIndexerSelector() { function createFilterBuilderPropsSelector() { return createSelector( - (state: AppState) => state.indexerIndex.filterBuilderProps, + (state) => state.indexerIndex.filterBuilderProps, (filterBuilderProps) => { return filterBuilderProps; } ); } -interface IndexerIndexFilterModalProps { - isOpen: boolean; -} - -export default function IndexerIndexFilterModal( - props: IndexerIndexFilterModalProps -) { +export default function IndexerIndexFilterModal(props) { const sectionItems = useSelector(createIndexerSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const customFilterType = 'indexerIndex'; @@ -37,7 +30,7 @@ export default function IndexerIndexFilterModal( const dispatch = useDispatch(); const dispatchSetFilter = useCallback( - (payload: unknown) => { + (payload) => { dispatch(setIndexerFilter(payload)); }, [dispatch] @@ -45,7 +38,6 @@ export default function IndexerIndexFilterModal( return ( { + (indexers) => { return indexers.items.map((s) => { const { protocol, privacy, enable } = s; diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx index 57ebf7b2f..0b6021bad 100644 --- a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx +++ b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx @@ -1,18 +1,10 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { CustomFilter } from 'App/State/AppState'; import FilterMenu from 'Components/Menu/FilterMenu'; import { align } from 'Helpers/Props'; import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal'; -interface IndexerIndexFilterMenuProps { - selectedFilterKey: string | number; - filters: object[]; - customFilters: CustomFilter[]; - isDisabled: boolean; - onFilterSelect(filterName: string): unknown; -} - -function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) { +function IndexerIndexFilterMenu(props) { const { selectedFilterKey, filters, @@ -34,6 +26,15 @@ function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) { ); } +IndexerIndexFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired, +}; + IndexerIndexFilterMenu.defaultProps = { showCustomFilters: false, }; diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx index 088cbca90..723db799f 100644 --- a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx +++ b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx @@ -1,19 +1,12 @@ +import PropTypes from 'prop-types'; import React from 'react'; import MenuContent from 'Components/Menu/MenuContent'; import SortMenu from 'Components/Menu/SortMenu'; import SortMenuItem from 'Components/Menu/SortMenuItem'; -import { align } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { align, sortDirections } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -interface IndexerIndexSortMenuProps { - sortKey?: string; - sortDirection?: SortDirection; - isDisabled: boolean; - onSortSelect(sortKey: string): unknown; -} - -function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) { +function IndexerIndexSortMenu(props) { const { sortKey, sortDirection, isDisabled, onSortSelect } = props; return ( @@ -86,4 +79,11 @@ function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) { ); } +IndexerIndexSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + isDisabled: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, +}; + export default IndexerIndexSortMenu; diff --git a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx index 0793af82d..3d241428a 100644 --- a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx @@ -7,10 +7,8 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; -import Indexer from 'Indexer/Indexer'; -import { bulkDeleteIndexers } from 'Store/Actions/indexerActions'; +import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions'; import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; -import translate from 'Utilities/String/translate'; import styles from './DeleteIndexerModalContent.css'; interface DeleteIndexerModalContentProps { @@ -21,21 +19,21 @@ interface DeleteIndexerModalContentProps { function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) { const { indexerIds, onModalClose } = props; - const allIndexers: Indexer[] = useSelector(createAllIndexersSelector()); + const allIndexer = useSelector(createAllIndexersSelector()); const dispatch = useDispatch(); - const indexers = useMemo((): Indexer[] => { - const indexerList = indexerIds.map((id) => { - return allIndexers.find((s) => s.id === id); - }) as Indexer[]; + const indexers = useMemo(() => { + const indexers = indexerIds.map((id) => { + return allIndexer.find((s) => s.id === id); + }); - return orderBy(indexerList, ['sortName']); - }, [indexerIds, allIndexers]); + return orderBy(indexers, ['sortTitle']); + }, [indexerIds, allIndexer]); const onDeleteIndexerConfirmed = useCallback(() => { dispatch( bulkDeleteIndexers({ - ids: indexerIds, + indexerIds, }) ); @@ -44,19 +42,17 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) { return ( - {translate('DeleteSelectedIndexers')} + Delete Selected Indexer
- {translate('DeleteSelectedIndexersMessageText', { - count: indexers.length, - })} + {`Are you sure you want to delete ${indexers.length} selected indexers?`}
    {indexers.map((s) => { return ( -
  • +
  • {s.name}
  • ); @@ -65,10 +61,10 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) { - + diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index 9d42aa389..05ad803b0 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -7,19 +7,13 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, sizes } from 'Helpers/Props'; +import { inputTypes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import styles from './EditIndexerModalContent.css'; interface SavePayload { enable?: boolean; appProfileId?: number; - priority?: number; - minimumSeeders?: number; - seedRatio?: number; - seedTime?: number; - packSeedTime?: number; - preferMagnetUrl?: boolean; } interface EditIndexerModalContentProps { @@ -31,25 +25,9 @@ interface EditIndexerModalContentProps { const NO_CHANGE = 'noChange'; const enableOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: 'true', - get value() { - return translate('Enabled'); - }, - }, - { - key: 'false', - get value() { - return translate('Disabled'); - }, - }, + { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, + { key: 'true', value: translate('Enabled') }, + { key: 'false', value: translate('Disabled') }, ]; function EditIndexerModalContent(props: EditIndexerModalContentProps) { @@ -57,18 +35,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { const [enable, setEnable] = useState(NO_CHANGE); const [appProfileId, setAppProfileId] = useState(NO_CHANGE); - const [priority, setPriority] = useState(null); - const [minimumSeeders, setMinimumSeeders] = useState( - null - ); - const [seedRatio, setSeedRatio] = useState(null); - const [seedTime, setSeedTime] = useState(null); - const [packSeedTime, setPackSeedTime] = useState( - null - ); - const [preferMagnetUrl, setPreferMagnetUrl] = useState< - null | string | boolean - >(null); const save = useCallback(() => { let hasChanges = false; @@ -84,56 +50,15 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { payload.appProfileId = appProfileId as number; } - if (priority !== null) { - hasChanges = true; - payload.priority = priority as number; - } - - if (minimumSeeders !== null) { - hasChanges = true; - payload.minimumSeeders = minimumSeeders as number; - } - - if (seedRatio !== null) { - hasChanges = true; - payload.seedRatio = seedRatio as number; - } - - if (seedTime !== null) { - hasChanges = true; - payload.seedTime = seedTime as number; - } - - if (packSeedTime !== null) { - hasChanges = true; - payload.packSeedTime = packSeedTime as number; - } - - if (preferMagnetUrl !== null) { - hasChanges = true; - payload.preferMagnetUrl = preferMagnetUrl === 'true'; - } - if (hasChanges) { onSavePress(payload); } onModalClose(); - }, [ - enable, - appProfileId, - priority, - minimumSeeders, - seedRatio, - seedTime, - packSeedTime, - preferMagnetUrl, - onSavePress, - onModalClose, - ]); + }, [enable, appProfileId, onSavePress, onModalClose]); const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { + ({ name, value }) => { switch (name) { case 'enable': setEnable(value); @@ -141,26 +66,8 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { case 'appProfileId': setAppProfileId(value); break; - case 'priority': - setPriority(value); - break; - case 'minimumSeeders': - setMinimumSeeders(value); - break; - case 'seedRatio': - setSeedRatio(value); - break; - case 'seedTime': - setSeedTime(value); - break; - case 'packSeedTime': - setPackSeedTime(value); - break; - case 'preferMagnetUrl': - setPreferMagnetUrl(value); - break; default: - console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); + console.warn('EditIndexerModalContent Unknown Input'); } }, [setEnable] @@ -174,10 +81,10 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { return ( - {translate('EditSelectedIndexers')} + {translate('Edit Selected Indexer')} - + {translate('Enable')} - + {translate('SyncProfile')} - - - {translate('IndexerPriority')} - - - - - - {translate('AppsMinimumSeeders')} - - - - - - {translate('SeedRatio')} - - - - - - {translate('SeedTime')} - - - - - - {translate('PackSeedTime')} - - - - - - {translate('PreferMagnetUrl')} - - -
    - {translate('CountIndexersSelected', { count: selectedCount })} + {translate('{0} indexers selected', selectedCount.toString())}
    diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx index d6fc776d6..c6b7d3fbe 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx @@ -1,13 +1,12 @@ import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; +import { SelectActionType, useSelect } from 'App/SelectContext'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; interface IndexerIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) { @@ -25,13 +24,15 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) { const onPress = useCallback(() => { selectDispatch({ - type: allSelected ? 'unselectAll' : 'selectAll', + type: allSelected + ? SelectActionType.UnselectAll + : SelectActionType.SelectAll, }); }, [allSelected, selectDispatch]); return isSelectMode ? ( diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx index f9a52ed30..682b8a3b0 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllMenuItem.tsx @@ -1,8 +1,7 @@ import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; +import { SelectActionType, useSelect } from 'App/SelectContext'; import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; interface IndexerIndexSelectAllMenuItemProps { label: string; @@ -26,13 +25,15 @@ function IndexerIndexSelectAllMenuItem( const onPressWrapper = useCallback(() => { selectDispatch({ - type: allSelected ? 'unselectAll' : 'selectAll', + type: allSelected + ? SelectActionType.UnselectAll + : SelectActionType.SelectAll, }); }, [allSelected, selectDispatch]); return isSelectMode ? ( diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx index 64fe8c1cb..5d9317859 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx @@ -1,13 +1,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import { useSelect } from 'App/SelectContext'; -import AppState from 'App/State/AppState'; +import { SelectActionType, useSelect } from 'App/SelectContext'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; -import usePrevious from 'Helpers/Hooks/usePrevious'; import { kinds } from 'Helpers/Props'; -import { bulkEditIndexers } from 'Store/Actions/indexerActions'; +import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions'; import translate from 'Utilities/String/translate'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; import DeleteIndexerModal from './Delete/DeleteIndexerModal'; @@ -15,18 +13,8 @@ import EditIndexerModal from './Edit/EditIndexerModal'; import TagsModal from './Tags/TagsModal'; import styles from './IndexerIndexSelectFooter.css'; -interface SavePayload { - enable?: boolean; - appProfileId?: number; - priority?: number; - minimumSeeders?: number; - seedRatio?: number; - seedTime?: number; - packSeedTime?: number; -} - -const indexersEditorSelector = createSelector( - (state: AppState) => state.indexers, +const seriesEditorSelector = createSelector( + (state) => state.indexers, (indexers) => { const { isSaving, isDeleting, deleteError } = indexers; @@ -39,9 +27,8 @@ const indexersEditorSelector = createSelector( ); function IndexerIndexSelectFooter() { - const { isSaving, isDeleting, deleteError } = useSelector( - indexersEditorSelector - ); + const { isSaving, isDeleting, deleteError } = + useSelector(seriesEditorSelector); const dispatch = useDispatch(); @@ -50,7 +37,6 @@ function IndexerIndexSelectFooter() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isSavingIndexer, setIsSavingIndexer] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); - const previousIsDeleting = usePrevious(isDeleting); const [selectState, selectDispatch] = useSelect(); const { selectedState } = selectState; @@ -70,14 +56,14 @@ function IndexerIndexSelectFooter() { }, [setIsEditModalOpen]); const onSavePress = useCallback( - (payload: SavePayload) => { + (payload) => { setIsSavingIndexer(true); setIsEditModalOpen(false); dispatch( - bulkEditIndexers({ + saveIndexerEditor({ ...payload, - ids: indexerIds, + indexerIds, }) ); }, @@ -93,13 +79,13 @@ function IndexerIndexSelectFooter() { }, [setIsTagsModalOpen]); const onApplyTagsPress = useCallback( - (tags: number[], applyTags: string) => { + (tags, applyTags) => { setIsSavingTags(true); setIsTagsModalOpen(false); dispatch( - bulkEditIndexers({ - ids: indexerIds, + saveIndexerEditor({ + indexerIds, tags, applyTags, }) @@ -124,10 +110,10 @@ function IndexerIndexSelectFooter() { }, [isSaving]); useEffect(() => { - if (previousIsDeleting && !isDeleting && !deleteError) { - selectDispatch({ type: 'unselectAll' }); + if (!isDeleting && !deleteError) { + selectDispatch({ type: SelectActionType.UnselectAll }); } - }, [previousIsDeleting, isDeleting, deleteError, selectDispatch]); + }, [isDeleting, deleteError, selectDispatch]); const anySelected = selectedCount > 0; @@ -148,7 +134,7 @@ function IndexerIndexSelectFooter() { isDisabled={!anySelected} onPress={onTagsPress} > - {translate('SetTags')} + {translate('Set Tags')}
@@ -165,7 +151,7 @@ function IndexerIndexSelectFooter() {
- {translate('CountIndexersSelected', { count: selectedCount })} + {translate('{0} indexers selected', selectedCount.toString())}
; + overflowComponent: React.FunctionComponent; onPress: () => void; } @@ -20,7 +20,7 @@ function IndexerIndexSelectModeButton( const onPressWrapper = useCallback(() => { if (isSelectMode) { selectDispatch({ - type: 'reset', + type: SelectActionType.Reset, }); } diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx index 8f1c0623f..f7d63950f 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectModeMenuItem.tsx @@ -1,6 +1,6 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import React, { useCallback } from 'react'; -import { useSelect } from 'App/SelectContext'; +import { SelectActionType, useSelect } from 'App/SelectContext'; import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem'; interface IndexerIndexSelectModeMenuItemProps { @@ -19,7 +19,7 @@ function IndexerIndexSelectModeMenuItem( const onPressWrapper = useCallback(() => { if (isSelectMode) { selectDispatch({ - type: 'reset', + type: SelectActionType.Reset, }); } diff --git a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx index 1964d271c..a8bbd09f7 100644 --- a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx @@ -1,7 +1,6 @@ -import { uniq } from 'lodash'; +import { concat, uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Tag } from 'App/State/TagsAppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -13,10 +12,8 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import Indexer from 'Indexer/Indexer'; import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import translate from 'Utilities/String/translate'; import styles from './TagsModalContent.css'; interface TagsModalContentProps { @@ -28,35 +25,29 @@ interface TagsModalContentProps { function TagsModalContent(props: TagsModalContentProps) { const { indexerIds, onModalClose, onApplyTagsPress } = props; - const allIndexers: Indexer[] = useSelector(createAllIndexersSelector()); - const tagList: Tag[] = useSelector(createTagsSelector()); + const allIndexers = useSelector(createAllIndexersSelector()); + const tagList = useSelector(createTagsSelector()); const [tags, setTags] = useState([]); const [applyTags, setApplyTags] = useState('add'); const indexerTags = useMemo(() => { - const tags = indexerIds.reduce((acc: number[], id) => { - const s = allIndexers.find((s) => s.id === id); + const indexers = indexerIds.map((id) => { + return allIndexers.find((s) => s.id === id); + }); - if (s) { - acc.push(...s.tags); - } - - return acc; - }, []); - - return uniq(tags); + return uniq(concat(...indexers.map((s) => s.tags))); }, [indexerIds, allIndexers]); const onTagsChange = useCallback( - ({ value }: { value: number[] }) => { + ({ value }) => { setTags(value); }, [setTags] ); const onApplyTagsChange = useCallback( - ({ value }: { value: string }) => { + ({ value }) => { setApplyTags(value); }, [setApplyTags] @@ -67,19 +58,19 @@ function TagsModalContent(props: TagsModalContentProps) { }, [tags, applyTags, onApplyTagsPress]); const applyTagsOptions = [ - { key: 'add', value: translate('Add') }, - { key: 'remove', value: translate('Remove') }, - { key: 'replace', value: translate('Replace') }, + { key: 'add', value: 'Add' }, + { key: 'remove', value: 'Remove' }, + { key: 'replace', value: 'Replace' }, ]; return ( - {translate('Tags')} + Tags
- {translate('Tags')} + Tags - {translate('ApplyTags')} + Apply Tags - {translate('Result')} + Result
{indexerTags.map((id) => { @@ -125,11 +116,7 @@ function TagsModalContent(props: TagsModalContentProps) { return (
); -} +}; function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; @@ -88,20 +88,22 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { isSelectMode, isSmallScreen, scrollerRef, - onCloneIndexerPress, } = props; const columns = useSelector(columnsSelector); - const listRef = useRef>(null); + const { showBanners } = useSelector(selectTableOptions); + const listRef: React.MutableRefObject = useRef(); const [measureRef, bounds] = useMeasure(); const [size, setSize] = useState({ width: 0, height: 0 }); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; - const rowHeight = 38; + const rowHeight = useMemo(() => { + return showBanners ? 70 : 38; + }, [showBanners]); useEffect(() => { - const current = scrollerRef?.current as HTMLElement; + const current = scrollerRef.current as HTMLElement; if (isSmallScreen) { setSize({ @@ -125,8 +127,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); useEffect(() => { - const currentScrollerRef = scrollerRef.current as HTMLElement; - const currentScrollListener = isSmallScreen ? window : currentScrollerRef; + const currentScrollListener = isSmallScreen ? window : scrollerRef.current; + const currentScrollerRef = scrollerRef.current; const handleScroll = throttle(() => { const { offsetTop = 0 } = currentScrollerRef; @@ -135,7 +137,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { ? getWindowScrollTopPosition() : currentScrollerRef.scrollTop) - offsetTop; - listRef.current?.scrollTo(scrollTop); + listRef.current.scrollTo(scrollTop); }, 10); currentScrollListener.addEventListener('scroll', handleScroll); @@ -164,8 +166,8 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { scrollTop += offset; } - listRef.current?.scrollTo(scrollTop); - scrollerRef?.current?.scrollTo(0, scrollTop); + listRef.current.scrollTo(scrollTop); + scrollerRef.current.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); @@ -177,6 +179,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { scrollDirection={ScrollDirection.Horizontal} > {Row} diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css index 839cd49ff..b9a3454c8 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css @@ -4,12 +4,6 @@ flex: 0 0 60px; } -.id { - composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; - - flex: 0 0 60px; -} - .sortName { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; @@ -18,12 +12,7 @@ .priority, .privacy, -.protocol, -.minimumSeeders, -.seedRatio, -.seedTime, -.packSeedTime, -.preferMagnetUrl { +.protocol { 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 020d61f27..af022da85 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts @@ -5,15 +5,9 @@ interface CssExports { 'added': string; 'appProfileId': string; 'capabilities': string; - 'id': string; - 'minimumSeeders': string; - 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; - 'seedRatio': string; - 'seedTime': string; 'sortName': string; 'status': string; 'tags': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx index 908be76b5..aa231533c 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { useSelect } from 'App/SelectContext'; +import { SelectActionType, useSelect } from 'App/SelectContext'; import IconButton from 'Components/Link/IconButton'; import Column from 'Components/Table/Column'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; @@ -14,11 +14,11 @@ import { setIndexerSort, setIndexerTableOption, } from 'Store/Actions/indexerIndexActions'; -import { CheckInputChanged } from 'typings/inputs'; import IndexerIndexTableOptions from './IndexerIndexTableOptions'; import styles from './IndexerIndexTableHeader.css'; interface IndexerIndexTableHeaderProps { + showBanners: boolean; columns: Column[]; sortKey?: string; sortDirection?: SortDirection; @@ -31,23 +31,23 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) { const [selectState, selectDispatch] = useSelect(); const onSortPress = useCallback( - (value: string) => { + (value) => { dispatch(setIndexerSort({ sortKey: value })); }, [dispatch] ); const onTableOptionChange = useCallback( - (payload: unknown) => { + (payload) => { dispatch(setIndexerTableOption(payload)); }, [dispatch] ); const onSelectAllChange = useCallback( - ({ value }: CheckInputChanged) => { + ({ value }) => { selectDispatch({ - type: value ? 'selectAll' : 'unselectAll', + type: value ? SelectActionType.SelectAll : SelectActionType.UnselectAll, }); }, [selectDispatch] @@ -92,18 +92,14 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) { return ( - {typeof label === 'function' ? label() : label} + {label} ); })} diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx index 3aa087790..d69bc1aed 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx @@ -1,11 +1,9 @@ -import React, { useCallback } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { useSelector } from 'react-redux'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import { inputTypes } from 'Helpers/Props'; -import { CheckInputChanged } from 'typings/inputs'; -import translate from 'Utilities/String/translate'; import selectTableOptions from './selectTableOptions'; interface IndexerIndexTableOptionsProps { @@ -20,7 +18,7 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { const { showSearchAction } = tableOptions; const onTableOptionChangeWrapper = useCallback( - ({ name, value }: CheckInputChanged) => { + ({ name, value }) => { onTableOptionChange({ tableOptions: { ...tableOptions, @@ -32,17 +30,19 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { ); return ( - - {translate('ShowSearch')} + + + Show Search - - + + + ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx index 1a2350302..4e8d1bd80 100644 --- a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx @@ -8,35 +8,11 @@ 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; redirect: boolean; - status?: IndexerStatus; + status: IndexerStatus; longDateFormat: string; timeFormat: string; component?: React.ElementType; @@ -54,14 +30,22 @@ function IndexerStatusCell(props: IndexerStatusCellProps) { ...otherProps } = props; + const enableKind = redirect ? kinds.INFO : kinds.SUCCESS; + const enableIcon = redirect ? icons.REDIRECT : icons.CHECK; + const enableTitle = redirect + ? 'Indexer is Enabled, Redirect is Enabled' + : 'Indexer is 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 c94e383b1..110c7e01c 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css @@ -11,7 +11,3 @@ 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 ba0cb260d..f3b389e3d 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.css.d.ts @@ -2,7 +2,6 @@ // 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 c1824452a..d1318678d 100644 --- a/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx +++ b/frontend/src/Indexer/Index/Table/ProtocolLabel.tsx @@ -1,13 +1,14 @@ import React from 'react'; import Label from 'Components/Label'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import styles from './ProtocolLabel.css'; interface ProtocolLabelProps { - protocol: DownloadProtocol; + protocol: string; } -function ProtocolLabel({ protocol }: ProtocolLabelProps) { +function ProtocolLabel(props: ProtocolLabelProps) { + const { protocol } = props; + const protocolName = protocol === 'usenet' ? 'nzb' : protocol; return ; diff --git a/frontend/src/Indexer/Index/Table/selectTableOptions.ts b/frontend/src/Indexer/Index/Table/selectTableOptions.ts index 56a00866d..1578c2cf8 100644 --- a/frontend/src/Indexer/Index/Table/selectTableOptions.ts +++ b/frontend/src/Indexer/Index/Table/selectTableOptions.ts @@ -1,8 +1,7 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; const selectTableOptions = createSelector( - (state: AppState) => state.indexerIndex.tableOptions, + (state) => state.indexerIndex.tableOptions, (tableOptions) => tableOptions ); diff --git a/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts b/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts index 4d6b9d803..12d042f7a 100644 --- a/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts +++ b/frontend/src/Indexer/Index/createIndexerIndexItemSelector.ts @@ -1,16 +1,26 @@ import { createSelector } from 'reselect'; +import Indexer from 'Indexer/Indexer'; import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector'; -import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector'; +import createIndexerSelector from 'Store/Selectors/createIndexerSelector'; import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; function createIndexerIndexItemSelector(indexerId: number) { return createSelector( - createIndexerSelectorForHook(indexerId), + createIndexerSelector(indexerId), createIndexerAppProfileSelector(indexerId), createIndexerStatusSelector(indexerId), createUISettingsSelector(), - (indexer, appProfile, status, uiSettings) => { + (indexer: Indexer, appProfile, status, uiSettings) => { + // If a series is deleted this selector may fire before the parent + // selectors, which will result in an undefined series, if that happens + // we want to return early here and again in the render function to avoid + // trying to show a series that has no information available. + + if (!indexer) { + return {}; + } + return { indexer, appProfile, diff --git a/frontend/src/Indexer/Indexer.ts b/frontend/src/Indexer/Indexer.ts index c363d472c..5ce83264b 100644 --- a/frontend/src/Indexer/Indexer.ts +++ b/frontend/src/Indexer/Indexer.ts @@ -1,5 +1,4 @@ import ModelBase from 'App/ModelBase'; -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; export interface IndexerStatus extends ModelBase { disabledTill: Date; @@ -25,46 +24,30 @@ export interface IndexerCapabilities extends ModelBase { categories: IndexerCategory[]; } -export type IndexerPrivacy = 'public' | 'semiPrivate' | 'private'; - export interface IndexerField extends ModelBase { - order: number; name: string; label: string; advanced: boolean; type: string; value: string; - privacy: string; } interface Indexer extends ModelBase { name: string; - definitionName: string; description: string; encoding: string; language: string; added: Date; enable: boolean; redirect: boolean; - supportsRss: boolean; - supportsSearch: boolean; - supportsRedirect: boolean; - supportsPagination: boolean; - protocol: DownloadProtocol; - privacy: IndexerPrivacy; + protocol: string; + privacy: string; priority: number; fields: IndexerField[]; tags: number[]; - sortName: string; status: IndexerStatus; capabilities: IndexerCapabilities; indexerUrls: string[]; - legacyUrls: string[]; - appProfileId: number; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; } export default Indexer; diff --git a/frontend/src/Indexer/IndexerTitleLink.tsx b/frontend/src/Indexer/IndexerTitleLink.tsx index be6e32db3..82f294d69 100644 --- a/frontend/src/Indexer/IndexerTitleLink.tsx +++ b/frontend/src/Indexer/IndexerTitleLink.tsx @@ -1,16 +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 { title, indexerId, onCloneIndexerPress } = props; + const { indexerName, indexerId } = props; const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false); @@ -25,17 +25,20 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) { return (
- {title} + {indexerName}
); } +IndexerTitleLink.propTypes = { + indexerName: PropTypes.string.isRequired, +}; + export default IndexerTitleLink; diff --git a/frontend/src/Indexer/Info/History/IndexerHistory.tsx b/frontend/src/Indexer/Info/History/IndexerHistory.tsx deleted file mode 100644 index 8cf62bde8..000000000 --- a/frontend/src/Indexer/Info/History/IndexerHistory.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import { IndexerHistoryAppState } from 'App/State/IndexerAppState'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { kinds } from 'Helpers/Props'; -import Indexer from 'Indexer/Indexer'; -import { - clearIndexerHistory, - fetchIndexerHistory, -} from 'Store/Actions/indexerHistoryActions'; -import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import translate from 'Utilities/String/translate'; -import IndexerHistoryRow from './IndexerHistoryRow'; - -const columns = [ - { - name: 'eventType', - isVisible: true, - }, - { - name: 'query', - label: () => translate('Query'), - isVisible: true, - }, - { - name: 'parameters', - label: () => translate('Parameters'), - isVisible: true, - }, - { - name: 'date', - label: () => translate('Date'), - isVisible: true, - }, - { - name: 'source', - label: () => translate('Source'), - isVisible: true, - }, - { - name: 'details', - label: () => translate('Details'), - isVisible: true, - }, -]; - -function createIndexerHistorySelector() { - return createSelector( - (state: AppState) => state.indexerHistory, - createUISettingsSelector(), - (state: AppState) => state.history.pageSize, - (indexerHistory: IndexerHistoryAppState, uiSettings, pageSize) => { - return { - ...indexerHistory, - shortDateFormat: uiSettings.shortDateFormat, - timeFormat: uiSettings.timeFormat, - pageSize, - }; - } - ); -} - -interface IndexerHistoryProps { - indexerId: number; -} - -function IndexerHistory(props: IndexerHistoryProps) { - const { - isFetching, - isPopulated, - error, - items, - shortDateFormat, - timeFormat, - pageSize, - } = useSelector(createIndexerHistorySelector()); - - const indexer = useSelector( - createIndexerSelectorForHook(props.indexerId) - ) as Indexer; - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch( - fetchIndexerHistory({ indexerId: props.indexerId, limit: pageSize }) - ); - - return () => { - dispatch(clearIndexerHistory()); - }; - }, [props, pageSize, dispatch]); - - const hasItems = !!items.length; - - if (isFetching) { - return ; - } - - if (!isFetching && !!error) { - return ( - {translate('IndexerHistoryLoadError')} - ); - } - - if (isPopulated && !hasItems && !error) { - return {translate('NoIndexerHistory')}; - } - - if (isPopulated && hasItems && !error) { - return ( - - - {items.map((item) => { - return ( - - ); - })} - -
- ); - } - - return null; -} - -export default IndexerHistory; diff --git a/frontend/src/Indexer/Info/History/IndexerHistoryRow.css b/frontend/src/Indexer/Info/History/IndexerHistoryRow.css deleted file mode 100644 index d8bba1fe7..000000000 --- a/frontend/src/Indexer/Info/History/IndexerHistoryRow.css +++ /dev/null @@ -1,23 +0,0 @@ -.query { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 120px; -} - -.elapsedTime, -.source { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 150px; -} - -.details { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - width: 70px; -} - -.parametersContent { - display: flex; - flex-wrap: wrap; -} diff --git a/frontend/src/Indexer/Info/History/IndexerHistoryRow.css.d.ts b/frontend/src/Indexer/Info/History/IndexerHistoryRow.css.d.ts deleted file mode 100644 index 28da0e31c..000000000 --- a/frontend/src/Indexer/Info/History/IndexerHistoryRow.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'details': string; - 'elapsedTime': string; - 'parametersContent': string; - 'query': string; - 'source': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx b/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx deleted file mode 100644 index 28d45654c..000000000 --- a/frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import IconButton from 'Components/Link/IconButton'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons } from 'Helpers/Props'; -import HistoryDetailsModal from 'History/Details/HistoryDetailsModal'; -import HistoryEventTypeCell from 'History/HistoryEventTypeCell'; -import { historyParameters } from 'History/HistoryRow'; -import HistoryRowParameter from 'History/HistoryRowParameter'; -import Indexer from 'Indexer/Indexer'; -import { HistoryData } from 'typings/History'; -import translate from 'Utilities/String/translate'; -import styles from './IndexerHistoryRow.css'; - -interface IndexerHistoryRowProps { - data: HistoryData; - date: string; - eventType: string; - successful: boolean; - indexer: Indexer; - shortDateFormat: string; - timeFormat: string; -} - -function IndexerHistoryRow(props: IndexerHistoryRowProps) { - const { - data, - date, - eventType, - successful, - indexer, - shortDateFormat, - timeFormat, - } = props; - - const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); - - const onDetailsModalPress = useCallback(() => { - setIsDetailsModalOpen(true); - }, [setIsDetailsModalOpen]); - - const onDetailsModalClose = useCallback(() => { - setIsDetailsModalOpen(false); - }, [setIsDetailsModalOpen]); - - const parameters = historyParameters.filter( - (parameter) => - parameter.key in data && data[parameter.key as keyof HistoryData] - ); - - return ( - - - - {data.query} - - -
- {parameters.map((parameter) => { - return ( - - ); - })} -
-
- - - - - {data.source ? data.source : null} - - - - - - - -
- ); -} - -export default IndexerHistoryRow; diff --git a/frontend/src/Indexer/Info/IndexerInfoModal.tsx b/frontend/src/Indexer/Info/IndexerInfoModal.tsx index c15af5247..df2ead86d 100644 --- a/frontend/src/Indexer/Info/IndexerInfoModal.tsx +++ b/frontend/src/Indexer/Info/IndexerInfoModal.tsx @@ -7,18 +7,16 @@ interface IndexerInfoModalProps { isOpen: boolean; indexerId: number; onModalClose(): void; - onCloneIndexerPress(id: number): void; } function IndexerInfoModal(props: IndexerInfoModalProps) { - const { isOpen, indexerId, onModalClose, onCloneIndexerPress } = props; + const { isOpen, onModalClose, indexerId } = props; return ( - + ); diff --git a/frontend/src/Indexer/Info/IndexerInfoModalContent.css b/frontend/src/Indexer/Info/IndexerInfoModalContent.css index 9e8b59a88..84fe0a573 100644 --- a/frontend/src/Indexer/Info/IndexerInfoModalContent.css +++ b/frontend/src/Indexer/Info/IndexerInfoModalContent.css @@ -9,41 +9,3 @@ margin-right: auto; } - -.tabs { - margin-top: -32px; -} - -.tabList { - margin: 0; - padding: 0; -} - -.tab { - position: relative; - bottom: -1px; - display: inline-block; - padding: 6px 12px; - border: 1px solid transparent; - border-top: none; - list-style: none; - cursor: pointer; -} - -.selectedTab { - border-color: var(--borderColor); - border-radius: 0 0 5px 5px; - background-color: rgba(239, 239, 239, 0.4); - color: var(--black); -} - -.tabContent { - margin-top: 20px; -} - -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - diff --git a/frontend/src/Indexer/Info/IndexerInfoModalContent.css.d.ts b/frontend/src/Indexer/Info/IndexerInfoModalContent.css.d.ts index c9f832fd9..48f09127f 100644 --- a/frontend/src/Indexer/Info/IndexerInfoModalContent.css.d.ts +++ b/frontend/src/Indexer/Info/IndexerInfoModalContent.css.d.ts @@ -3,12 +3,6 @@ interface CssExports { 'deleteButton': string; 'description': string; - 'modalFooter': string; - 'selectedTab': string; - 'tab': string; - 'tabContent': string; - 'tabList': string; - 'tabs': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx b/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx index 344d91a98..5249e980a 100644 --- a/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx +++ b/frontend/src/Indexer/Info/IndexerInfoModalContent.tsx @@ -1,7 +1,6 @@ -import { uniqBy } from 'lodash'; import React, { useCallback, useState } from 'react'; -import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; -import Alert from 'Components/Alert'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; @@ -14,31 +13,35 @@ 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 TableRowCell from 'Components/Table/Cells/TableRowCell'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableRow from 'Components/Table/TableRow'; import TagListConnector from 'Components/TagListConnector'; import { kinds } from 'Helpers/Props'; import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; -import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel'; -import Indexer, { IndexerCapabilities } from 'Indexer/Indexer'; -import useIndexer from 'Indexer/useIndexer'; +import Indexer from 'Indexer/Indexer'; +import createIndexerSelector from 'Store/Selectors/createIndexerSelector'; import translate from 'Utilities/String/translate'; -import IndexerHistory from './History/IndexerHistory'; import styles from './IndexerInfoModalContent.css'; -const TABS = ['details', 'categories', 'history', 'stats']; +function createIndexerInfoItemSelector(indexerId: number) { + return createSelector( + createIndexerSelector(indexerId), + (indexer: Indexer) => { + return { + indexer, + }; + } + ); +} interface IndexerInfoModalContentProps { indexerId: number; onModalClose(): void; - onCloneIndexerPress(id: number): void; } function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { - const { indexerId, onModalClose, onCloneIndexerPress } = props; + const { indexer } = useSelector( + createIndexerInfoItemSelector(props.indexerId) + ); const { id, @@ -50,329 +53,214 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { fields, tags, protocol, - privacy, - capabilities = {} as IndexerCapabilities, - } = useIndexer(indexerId) as Indexer; + capabilities, + } = indexer; - const [selectedTab, setSelectedTab] = useState(TABS[0]); - const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); - const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = - useState(false); - - const handleTabSelect = useCallback( - (selectedIndex: number) => { - const selectedTab = TABS[selectedIndex]; - setSelectedTab(selectedTab); - }, - [setSelectedTab] - ); - - const handleEditIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(true); - }, [setIsEditIndexerModalOpen]); - - const handleEditIndexerModalClose = useCallback(() => { - setIsEditIndexerModalOpen(false); - }, [setIsEditIndexerModalOpen]); - - const handleDeleteIndexerPress = useCallback(() => { - setIsEditIndexerModalOpen(false); - setIsDeleteIndexerModalOpen(true); - }, [setIsDeleteIndexerModalOpen]); - - const handleDeleteIndexerModalClose = useCallback(() => { - setIsDeleteIndexerModalOpen(false); - onModalClose(); - }, [setIsDeleteIndexerModalOpen, onModalClose]); - - const handleCloneIndexerPressWrapper = useCallback(() => { - onCloneIndexerPress(id); - onModalClose(); - }, [id, onCloneIndexerPress, onModalClose]); + const { onModalClose } = props; 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; + const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = + useState(false); + + const onEditIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(true); + }, [setIsEditIndexerModalOpen]); + + const onEditIndexerModalClose = useCallback(() => { + setIsEditIndexerModalOpen(false); + }, [setIsEditIndexerModalOpen]); + + const onDeleteIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(false); + setIsDeleteIndexerModalOpen(true); + }, [setIsDeleteIndexerModalOpen]); + + const onDeleteIndexerModalClose = useCallback(() => { + setIsDeleteIndexerModalOpen(false); + onModalClose(); + }, [setIsDeleteIndexerModalOpen, onModalClose]); + return ( {`${name}`} - - - - {translate('Details')} - - - - {translate('Categories')} - - - - {translate('History')} - - - -
-
-
- - - - - - : '-'} - /> - {vipExpiration ? ( - - ) : null} - - {translate('IndexerSite')} - - - {indexerUrl ? ( - {indexerUrl} - ) : ( - '-' - )} - - - {protocol === 'usenet' - ? translate('NewznabUrl') - : translate('TorznabUrl')} - - - {`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`} - - {tags.length > 0 ? ( - <> - - {translate('Tags')} - - - - - - ) : null} - -
-
- -
-
- - - 0 ? ( - - ) : ( - translate('NotSupported') - ) - } - /> - 0 - ? capabilities.tvSearchParams.map((p) => { - return ( - - ); - }) - : translate('NotSupported') - } - /> - 0 - ? capabilities.movieSearchParams.map((p) => { - return ( - - ); - }) - : translate('NotSupported') - } - /> - 0 - ? capabilities.bookSearchParams.map((p) => { - return ( - - ); - }) - : translate('NotSupported') - } - /> - 0 - ? capabilities.musicSearchParams.map((p) => { - return ( - - ); - }) - : translate('NotSupported') - } - /> - -
-
-
-
- -
- {capabilities?.categories?.length > 0 ? ( -
- - {uniqBy(capabilities.categories, 'id') - .sort((a, b) => a.id - b.id) - .map((category) => { +
+
+ + + + + + {vipExpiration ? ( + + ) : null} + + {translate('IndexerSite')} + + + + {baseUrl.replace(/(:\/\/)api\./, '$1')} + + + {`${ + protocol === 'usenet' ? 'Newznab' : 'Torznab' + } Url`} + + {`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`} + + {tags.length > 0 ? ( + <> + + {translate('Tags')} + + + + + + ) : null} + +
+
+
+
+ + + + {capabilities.searchParams[0]} + + ) + } + /> + { return ( - - - {category.id} - {category.name} - - {category?.subCategories?.length > 0 - ? uniqBy(category.subCategories, 'id') - .sort((a, b) => a.id - b.id) - .map((subCategory) => { - return ( - - - {subCategory.id} - - - {subCategory.name} - - - ); - }) - : null} - + ); - })} -
-
- ) : ( - - {translate('NoIndexerCategories')} - - )} -
-
- -
- -
-
-
+ }) + } + /> + { + return ( + + ); + }) + } + /> + { + return ( + + ); + }) + } + /> + { + return ( + + ); + }) + } + /> + +
+ - -
- - -
-
- - -
+ + + + ); diff --git a/frontend/src/Indexer/NoIndexer.css b/frontend/src/Indexer/NoIndexer.css index 4ad534de3..38a01f391 100644 --- a/frontend/src/Indexer/NoIndexer.css +++ b/frontend/src/Indexer/NoIndexer.css @@ -1,6 +1,4 @@ .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.js similarity index 54% rename from frontend/src/Indexer/NoIndexer.tsx rename to frontend/src/Indexer/NoIndexer.js index bf5afa1fe..f94df7902 100644 --- a/frontend/src/Indexer/NoIndexer.tsx +++ b/frontend/src/Indexer/NoIndexer.js @@ -1,23 +1,23 @@ +import PropTypes from 'prop-types'; 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'; import styles from './NoIndexer.css'; -interface NoIndexerProps { - totalItems: number; - onAddIndexerPress(): void; -} - -function NoIndexer(props: NoIndexerProps) { - const { totalItems, onAddIndexerPress } = props; +function NoIndexer(props) { + const { + totalItems, + onAddIndexerPress + } = props; if (totalItems > 0) { return ( - - {translate('AllIndexersHiddenDueToFilter')} - +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
); } @@ -28,7 +28,10 @@ function NoIndexer(props: NoIndexerProps) {
-
@@ -36,4 +39,9 @@ function NoIndexer(props: NoIndexerProps) { ); } +NoIndexer.propTypes = { + totalItems: PropTypes.number.isRequired, + onAddIndexerPress: PropTypes.func.isRequired +}; + export default NoIndexer; diff --git a/frontend/src/Indexer/Stats/IndexerStats.css b/frontend/src/Indexer/Stats/IndexerStats.css deleted file mode 100644 index 975f5ddae..000000000 --- a/frontend/src/Indexer/Stats/IndexerStats.css +++ /dev/null @@ -1,52 +0,0 @@ -.fullWidthChart { - display: inline-block; - width: 100%; -} - -.halfWidthChart { - display: inline-block; - width: 50%; -} - -.quarterWidthChart { - display: inline-block; - width: 25%; -} - -.chartContainer { - margin: 5px; - padding: 15px 25px; - height: 300px; - border-radius: 10px; - background-color: var(--chartBackgroundColor); -} - -.statContainer { - margin: 5px; - padding: 15px 25px; - height: 150px; - border-radius: 10px; - background-color: var(--chartBackgroundColor); -} - -.statTitle { - font-weight: bold; - font-size: 14px; -} - -.stat { - font-weight: bold; - font-size: 60px; -} - -@media only screen and (max-width: $breakpointSmall) { - .halfWidthChart { - display: inline-block; - width: 100%; - } - - .quarterWidthChart { - display: inline-block; - width: 100%; - } -} diff --git a/frontend/src/Indexer/Stats/IndexerStats.css.d.ts b/frontend/src/Indexer/Stats/IndexerStats.css.d.ts deleted file mode 100644 index e2aae98c6..000000000 --- a/frontend/src/Indexer/Stats/IndexerStats.css.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'chartContainer': string; - 'fullWidthChart': string; - 'halfWidthChart': string; - 'quarterWidthChart': string; - 'stat': string; - 'statContainer': string; - 'statTitle': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Indexer/Stats/IndexerStats.tsx b/frontend/src/Indexer/Stats/IndexerStats.tsx deleted file mode 100644 index bccd49cbe..000000000 --- a/frontend/src/Indexer/Stats/IndexerStats.tsx +++ /dev/null @@ -1,373 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import IndexerStatsAppState from 'App/State/IndexerStatsAppState'; -import Alert from 'Components/Alert'; -import BarChart from 'Components/Chart/BarChart'; -import DoughnutChart from 'Components/Chart/DoughnutChart'; -import StackedBarChart from 'Components/Chart/StackedBarChart'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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, icons, kinds } from 'Helpers/Props'; -import { - fetchIndexerStats, - setIndexerStatsFilter, -} from 'Store/Actions/indexerStatsActions'; -import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; -import { - IndexerStatsHost, - IndexerStatsIndexer, - IndexerStatsUserAgent, -} from 'typings/IndexerStats'; -import abbreviateNumber from 'Utilities/Number/abbreviateNumber'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import IndexerStatsFilterModal from './IndexerStatsFilterModal'; -import styles from './IndexerStats.css'; - -function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) { - const statistics = [...indexerStats].sort((a, b) => - a.averageResponseTime === b.averageResponseTime - ? b.averageGrabResponseTime - a.averageGrabResponseTime - : b.averageResponseTime - a.averageResponseTime - ); - - 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) => ({ - label: indexer.indexerName, - value: - (indexer.numberOfFailedQueries + - indexer.numberOfFailedRssQueries + - indexer.numberOfFailedAuthQueries + - indexer.numberOfFailedGrabs) / - (indexer.numberOfQueries + - indexer.numberOfRssQueries + - indexer.numberOfAuthQueries + - indexer.numberOfGrabs), - })) - .filter((s) => s.value > 0); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) { - 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: statistics.map((indexer) => indexer.numberOfQueries), - }, - { - label: translate('RssQueries'), - data: statistics.map((indexer) => indexer.numberOfRssQueries), - }, - { - label: translate('AuthQueries'), - data: statistics.map((indexer) => indexer.numberOfAuthQueries), - }, - ], - }; -} - -function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) { - const data = [...indexerStats] - .map((indexer) => ({ - label: indexer.indexerName, - value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs, - })) - .filter((s) => s.value > 0); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfGrabs, - })); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.userAgent ? indexer.userAgent : 'Other', - value: indexer.numberOfQueries, - })); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -function getHostGrabsData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfGrabs, - })); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -function getHostQueryData(indexerStats: IndexerStatsHost[]) { - const data = indexerStats.map((indexer) => ({ - label: indexer.host ? indexer.host : 'Other', - value: indexer.numberOfQueries, - })); - - data.sort((a, b) => b.value - a.value); - - return data; -} - -const indexerStatsSelector = () => { - return createSelector( - (state: AppState) => state.indexerStats, - createCustomFiltersSelector('indexerStats'), - (indexerStats: IndexerStatsAppState, customFilters) => { - return { - ...indexerStats, - customFilters, - }; - } - ); -}; - -function IndexerStats() { - const { - isFetching, - isPopulated, - item, - error, - filters, - customFilters, - selectedFilterKey, - } = useSelector(indexerStatsSelector()); - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(fetchIndexerStats()); - }, [dispatch]); - - const onRefreshPress = useCallback(() => { - dispatch(fetchIndexerStats()); - }, [dispatch]); - - const onFilterSelect = useCallback( - (value: string) => { - dispatch(setIndexerStatsFilter({ selectedFilterKey: value })); - }, - [dispatch] - ); - - const isLoaded = !error && isPopulated; - const indexerCount = item.indexers?.length ?? 0; - const userAgentCount = item.userAgents?.length ?? 0; - const queryCount = - item.indexers?.reduce((total, indexer) => { - return ( - total + - indexer.numberOfQueries + - indexer.numberOfRssQueries + - indexer.numberOfAuthQueries - ); - }, 0) ?? 0; - const grabCount = - item.indexers?.reduce((total, indexer) => { - return total + indexer.numberOfGrabs; - }, 0) ?? 0; - - return ( - - - - - - - - - - - - {isFetching && !isPopulated && } - - {!isFetching && !!error && ( - - {getErrorMessage(error, 'Failed to load indexer stats from API')} - - )} - - {isLoaded && ( -
-
-
-
- {translate('ActiveIndexers')} -
-
{indexerCount}
-
-
-
-
-
- {translate('TotalQueries')} -
-
- {abbreviateNumber(queryCount)} -
-
-
-
-
-
- {translate('TotalGrabs')} -
-
{abbreviateNumber(grabCount)}
-
-
-
-
-
- {translate('ActiveApps')} -
-
{userAgentCount}
-
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
- )} -
-
- ); -} - -export default IndexerStats; diff --git a/frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx b/frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx deleted file mode 100644 index 6e3a49dfb..000000000 --- a/frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import FilterModal from 'Components/Filter/FilterModal'; -import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions'; - -function createIndexerStatsSelector() { - return createSelector( - (state: AppState) => state.indexerStats.item, - (indexerStats) => { - return indexerStats; - } - ); -} - -function createFilterBuilderPropsSelector() { - return createSelector( - (state: AppState) => state.indexerStats.filterBuilderProps, - (filterBuilderProps) => { - return filterBuilderProps; - } - ); -} - -interface IndexerStatsFilterModalProps { - isOpen: boolean; -} - -export default function IndexerStatsFilterModal( - props: IndexerStatsFilterModalProps -) { - const sectionItems = [useSelector(createIndexerStatsSelector())]; - const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); - const customFilterType = 'indexerStats'; - - const dispatch = useDispatch(); - - const dispatchSetFilter = useCallback( - (payload: unknown) => { - dispatch(setIndexerStatsFilter(payload)); - }, - [dispatch] - ); - - return ( - - ); -} diff --git a/frontend/src/Indexer/Stats/Stats.css b/frontend/src/Indexer/Stats/Stats.css new file mode 100644 index 000000000..249dcc448 --- /dev/null +++ b/frontend/src/Indexer/Stats/Stats.css @@ -0,0 +1,22 @@ +.fullWidthChart { + display: inline-block; + padding: 15px 25px; + width: 100%; + height: 300px; +} + +.halfWidthChart { + display: inline-block; + padding: 15px 25px; + width: 50%; + height: 300px; +} + +@media only screen and (max-width: $breakpointSmall) { + .halfWidthChart { + display: inline-block; + padding: 15px 25px; + width: 100%; + height: 300px; + } +} diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.css.d.ts b/frontend/src/Indexer/Stats/Stats.css.d.ts similarity index 74% rename from frontend/src/Components/Table/Cells/TableSelectCell.css.d.ts rename to frontend/src/Indexer/Stats/Stats.css.d.ts index b6aee3c85..ce2364202 100644 --- a/frontend/src/Components/Table/Cells/TableSelectCell.css.d.ts +++ b/frontend/src/Indexer/Stats/Stats.css.d.ts @@ -1,8 +1,8 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'input': string; - 'selectCell': string; + 'fullWidthChart': string; + 'halfWidthChart': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Indexer/Stats/Stats.js b/frontend/src/Indexer/Stats/Stats.js new file mode 100644 index 000000000..b063d638c --- /dev/null +++ b/frontend/src/Indexer/Stats/Stats.js @@ -0,0 +1,260 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import BarChart from 'Components/Chart/BarChart'; +import DoughnutChart from 'Components/Chart/DoughnutChart'; +import StackedBarChart from 'Components/Chart/StackedBarChart'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import { align, kinds } from 'Helpers/Props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import StatsFilterMenu from './StatsFilterMenu'; +import styles from './Stats.css'; + +function getAverageResponseTimeData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.indexerName, + value: indexer.averageResponseTime + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getFailureRateData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.indexerName, + value: (indexer.numberOfFailedQueries + indexer.numberOfFailedRssQueries + indexer.numberOfFailedAuthQueries + indexer.numberOfFailedGrabs) / + (indexer.numberOfQueries + indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs) + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getTotalRequestsData(indexerStats) { + const data = { + labels: indexerStats.map((indexer) => indexer.indexerName), + datasets: [ + { + label: 'Search Queries', + data: indexerStats.map((indexer) => indexer.numberOfQueries) + }, + { + label: 'Rss Queries', + data: indexerStats.map((indexer) => indexer.numberOfRssQueries) + }, + { + label: 'Auth Queries', + data: indexerStats.map((indexer) => indexer.numberOfAuthQueries) + } + ] + }; + + return data; +} + +function getNumberGrabsData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.indexerName, + value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getUserAgentGrabsData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfGrabs + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getUserAgentQueryData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.userAgent ? indexer.userAgent : 'Other', + value: indexer.numberOfQueries + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getHostGrabsData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfGrabs + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function getHostQueryData(indexerStats) { + const data = indexerStats.map((indexer) => { + return { + label: indexer.host ? indexer.host : 'Other', + value: indexer.numberOfQueries + }; + }); + + data.sort((a, b) => { + return b.value - a.value; + }); + + return data; +} + +function Stats(props) { + const { + item, + isFetching, + isPopulated, + error, + filters, + selectedFilterKey, + onFilterSelect + } = props; + + const isLoaded = !!(!error && isPopulated); + + return ( + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
+ {getErrorMessage(error, 'Failed to load indexer stats from API')} +
+ } + + { + isLoaded && +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ } +
+
+ ); +} + +Stats.propTypes = { + item: PropTypes.object.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.string.isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + onFilterSelect: PropTypes.func.isRequired, + error: PropTypes.object, + data: PropTypes.object +}; + +export default Stats; diff --git a/frontend/src/Indexer/Stats/StatsConnector.js b/frontend/src/Indexer/Stats/StatsConnector.js new file mode 100644 index 000000000..006716953 --- /dev/null +++ b/frontend/src/Indexer/Stats/StatsConnector.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions'; +import Stats from './Stats'; + +function createMapStateToProps() { + return createSelector( + (state) => state.indexerStats, + (indexerStats) => indexerStats + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onFilterSelect(selectedFilterKey) { + dispatch(setIndexerStatsFilter({ selectedFilterKey })); + }, + dispatchFetchIndexerStats() { + dispatch(fetchIndexerStats()); + } + }; +} + +class StatsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchIndexerStats(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +StatsConnector.propTypes = { + dispatchFetchIndexerStats: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector); diff --git a/frontend/src/Indexer/Stats/StatsFilterMenu.js b/frontend/src/Indexer/Stats/StatsFilterMenu.js new file mode 100644 index 000000000..283159b7e --- /dev/null +++ b/frontend/src/Indexer/Stats/StatsFilterMenu.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import { align } from 'Helpers/Props'; + +function StatsFilterMenu(props) { + const { + selectedFilterKey, + filters, + isDisabled, + onFilterSelect + } = props; + + return ( + + ); +} + +StatsFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired +}; + +StatsFilterMenu.defaultProps = { + showCustomFilters: false +}; + +export default StatsFilterMenu; diff --git a/frontend/src/Indexer/Stats/StatsFilterModalConnector.js b/frontend/src/Indexer/Stats/StatsFilterModalConnector.js new file mode 100644 index 000000000..53bf2ed3c --- /dev/null +++ b/frontend/src/Indexer/Stats/StatsFilterModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.indexerStats.items, + (state) => state.indexerStats.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps, + customFilterType: 'indexerStats' + }; + } + ); +} + +const mapDispatchToProps = { + dispatchSetFilter: setIndexerStatsFilter +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/Indexer/useIndexer.ts b/frontend/src/Indexer/useIndexer.ts deleted file mode 100644 index a1b2ffa9d..000000000 --- a/frontend/src/Indexer/useIndexer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -export function createIndexerSelector(indexerId?: number) { - return createSelector( - (state: AppState) => 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/Menus/SearchIndexFilterMenu.tsx b/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx index 9a8c243b4..52806ff83 100644 --- a/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx +++ b/frontend/src/Search/Menus/SearchIndexFilterMenu.tsx @@ -1,18 +1,10 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { CustomFilter } from 'App/State/AppState'; import FilterMenu from 'Components/Menu/FilterMenu'; import { align } from 'Helpers/Props'; import SearchIndexFilterModalConnector from 'Search/SearchIndexFilterModalConnector'; -interface SearchIndexFilterMenuProps { - selectedFilterKey: string | number; - filters: object[]; - customFilters: CustomFilter[]; - isDisabled: boolean; - onFilterSelect(filterName: string): unknown; -} - -function SearchIndexFilterMenu(props: SearchIndexFilterMenuProps) { +function SearchIndexFilterMenu(props) { const { selectedFilterKey, filters, @@ -34,6 +26,15 @@ function SearchIndexFilterMenu(props: SearchIndexFilterMenuProps) { ); } +SearchIndexFilterMenu.propTypes = { + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired, +}; + SearchIndexFilterMenu.defaultProps = { showCustomFilters: false, }; diff --git a/frontend/src/Search/Menus/SearchIndexSortMenu.tsx b/frontend/src/Search/Menus/SearchIndexSortMenu.tsx index af4042283..302ef6a10 100644 --- a/frontend/src/Search/Menus/SearchIndexSortMenu.tsx +++ b/frontend/src/Search/Menus/SearchIndexSortMenu.tsx @@ -1,19 +1,12 @@ +import PropTypes from 'prop-types'; import React from 'react'; import MenuContent from 'Components/Menu/MenuContent'; import SortMenu from 'Components/Menu/SortMenu'; import SortMenuItem from 'Components/Menu/SortMenuItem'; -import { align } from 'Helpers/Props'; -import SortDirection from 'Helpers/Props/SortDirection'; +import { align, sortDirections } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; -interface SearchIndexSortMenuProps { - sortKey?: string; - sortDirection?: SortDirection; - isDisabled: boolean; - onSortSelect(sortKey: string): unknown; -} - -function SearchIndexSortMenu(props: SearchIndexSortMenuProps) { +function SearchIndexSortMenu(props) { const { sortKey, sortDirection, isDisabled, onSortSelect } = props; return ( @@ -104,4 +97,11 @@ function SearchIndexSortMenu(props: SearchIndexSortMenuProps) { ); } +SearchIndexSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + isDisabled: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, +}; + export default SearchIndexSortMenu; diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css b/frontend/src/Search/Mobile/SearchIndexOverview.css index e29ff1ef9..4e184bd0a 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css @@ -47,42 +47,3 @@ $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 68256eb25..266cf7fca 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts +++ b/frontend/src/Search/Mobile/SearchIndexOverview.css.d.ts @@ -4,13 +4,9 @@ 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 new file mode 100644 index 000000000..7294d1f6c --- /dev/null +++ b/frontend/src/Search/Mobile/SearchIndexOverview.js @@ -0,0 +1,202 @@ +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 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 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, + categories, + seeders, + leechers, + size, + age, + ageHours, + ageMinutes, + indexer, + rowHeight, + isSmallScreen, + isGrabbed, + isGrabbing, + grabError + } = this.props; + + const contentHeight = getContentHeight(rowHeight, isSmallScreen); + + return ( +
+
+
+
+
+ + + + +
+ +
+ + + +
+
+
+ {indexer} +
+
+ + + { + protocol === 'torrent' && + + } + + + + + + +
+
+
+
+ ); + } +} + +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.isRequired, + 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 deleted file mode 100644 index 21a42d70c..000000000 --- a/frontend/src/Search/Mobile/SearchIndexOverview.tsx +++ /dev/null @@ -1,264 +0,0 @@ -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/Mobile/SearchIndexOverviews.js b/frontend/src/Search/Mobile/SearchIndexOverviews.js index 671c8e9fd..7fadb51e0 100644 --- a/frontend/src/Search/Mobile/SearchIndexOverviews.js +++ b/frontend/src/Search/Mobile/SearchIndexOverviews.js @@ -195,7 +195,7 @@ class SearchIndexOverviews extends Component { SearchIndexOverviews.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, - scrollTop: PropTypes.number, + scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, scroller: PropTypes.instanceOf(Element).isRequired, showRelativeDates: PropTypes.bool.isRequired, diff --git a/frontend/src/Search/NoSearchResults.css b/frontend/src/Search/NoSearchResults.css index f17dd633e..eff6272f7 100644 --- a/frontend/src/Search/NoSearchResults.css +++ b/frontend/src/Search/NoSearchResults.css @@ -1,6 +1,4 @@ .message { - composes: alert from '~Components/Alert.css'; - margin-top: 10px; margin-bottom: 30px; text-align: center; diff --git a/frontend/src/Search/NoSearchResults.js b/frontend/src/Search/NoSearchResults.js new file mode 100644 index 000000000..03fce4be9 --- /dev/null +++ b/frontend/src/Search/NoSearchResults.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import translate from 'Utilities/String/translate'; +import styles from './NoSearchResults.css'; + +function NoSearchResults(props) { + const { totalItems } = props; + + if (totalItems > 0) { + return ( +
+
+ {translate('AllIndexersHiddenDueToFilter')} +
+
+ ); + } + + return ( +
+
+ {translate('NoSearchResultsFound')} +
+
+ ); +} + +NoSearchResults.propTypes = { + totalItems: PropTypes.number.isRequired +}; + +export default NoSearchResults; diff --git a/frontend/src/Search/NoSearchResults.tsx b/frontend/src/Search/NoSearchResults.tsx deleted file mode 100644 index 46fbc85e0..000000000 --- a/frontend/src/Search/NoSearchResults.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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'; - -interface NoSearchResultsProps { - totalItems: number; -} - -function NoSearchResults(props: NoSearchResultsProps) { - const { totalItems } = props; - - if (totalItems > 0) { - return ( - - {translate('AllSearchResultsHiddenByFilter')} - - ); - } - - return ( - - {translate('NoSearchResultsFound')} - - ); -} - -export default NoSearchResults; diff --git a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx b/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx deleted file mode 100644 index 7d623decd..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 63e15808f..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientModalContent.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 6525db977..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css +++ /dev/null @@ -1,6 +0,0 @@ -.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 deleted file mode 100644 index 10c2d3948..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 6f98d60b4..000000000 --- a/frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index bd4d2f788..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css +++ /dev/null @@ -1,17 +0,0 @@ -.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 deleted file mode 100644 index dd3ac4575..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 82d6bd812..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchData.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 16d62ea7c..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index a5b4b8d52..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css +++ /dev/null @@ -1,49 +0,0 @@ -.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 deleted file mode 100644 index 79c77d6b5..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index fbe0ec450..000000000 --- a/frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx +++ /dev/null @@ -1,150 +0,0 @@ -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/QueryParameterModal.js b/frontend/src/Search/QueryParameterModal.js index cd7b8e191..df06648a2 100644 --- a/frontend/src/Search/QueryParameterModal.js +++ b/frontend/src/Search/QueryParameterModal.js @@ -14,11 +14,11 @@ import QueryParameterOption from './QueryParameterOption'; import styles from './QueryParameterModal.css'; const searchOptions = [ - { key: 'search', value: () => translate('BasicSearch') }, - { key: 'tvsearch', value: () => translate('TvSearch') }, - { key: 'movie', value: () => translate('MovieSearch') }, - { key: 'music', value: () => translate( 'AudioSearch') }, - { key: 'book', value: () => translate('BookSearch') } + { key: 'search', value: 'Basic Search' }, + { key: 'tvsearch', value: 'TV Search' }, + { key: 'movie', value: 'Movie Search' }, + { key: 'music', value: 'Audio Search' }, + { key: 'book', value: 'Book Search' } ]; const seriesTokens = [ @@ -94,8 +94,8 @@ class QueryParameterModal extends Component { const newValue = `${start}${tokenValue}${end}`; onSearchInputChange({ name, value: newValue }); - this._selectionStart = newValue.length; - this._selectionEnd = newValue.length; + this._selectionStart = newValue.length - 1; + this._selectionEnd = newValue.length - 1; } }; diff --git a/frontend/src/Search/SearchFooter.css b/frontend/src/Search/SearchFooter.css index 65121e5e3..54e68660b 100644 --- a/frontend/src/Search/SearchFooter.css +++ b/frontend/src/Search/SearchFooter.css @@ -24,8 +24,7 @@ flex-grow: 1; } -.searchButton, -.grabReleasesButton { +.searchButton { composes: button from '~Components/Link/SpinnerButton.css'; margin-left: 25px; @@ -33,20 +32,18 @@ } .selectedReleasesLabel { - margin-bottom: 5px; + margin-bottom: 3px; text-align: right; font-weight: bold; } @media only screen and (max-width: $breakpointSmall) { - .inputContainer, - .indexerContainer { + .inputContainer { margin-right: 0; } .buttonContainer { justify-content: flex-start; - margin-top: 10px; } .buttonContainerContent { @@ -55,20 +52,5 @@ .buttons { justify-content: space-between; - flex-direction: column; - gap: 10px; - } - - .grabReleasesButton, - .searchButton { - margin-left: 0; - } - - .grabReleasesButton { - display: none; - } - - .selectedReleasesLabel { - text-align: center; } } diff --git a/frontend/src/Search/SearchFooter.css.d.ts b/frontend/src/Search/SearchFooter.css.d.ts index e72f81320..8bf441cf4 100644 --- a/frontend/src/Search/SearchFooter.css.d.ts +++ b/frontend/src/Search/SearchFooter.css.d.ts @@ -4,7 +4,6 @@ interface CssExports { 'buttonContainer': string; 'buttonContainerContent': string; 'buttons': string; - 'grabReleasesButton': string; 'indexerContainer': string; 'inputContainer': string; 'searchButton': string; diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index 872328446..86f26fc51 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -24,26 +24,23 @@ class SearchFooter extends Component { super(props, context); const { - defaultSearchQueryParams, defaultIndexerIds, defaultCategories, defaultSearchQuery, - defaultSearchType, - defaultSearchLimit, - defaultSearchOffset + defaultSearchType } = props; this.state = { - searchIndexerIds: defaultSearchQueryParams.searchIndexerIds ?? defaultIndexerIds, - searchCategories: defaultSearchQueryParams.searchCategories ?? defaultCategories, - searchQuery: (defaultSearchQueryParams.searchQuery ?? defaultSearchQuery) || '', - searchType: defaultSearchQueryParams.searchType ?? defaultSearchType, - searchLimit: defaultSearchQueryParams.searchLimit ?? defaultSearchLimit, - searchOffset: defaultSearchQueryParams.searchOffset ?? defaultSearchOffset, - newSearch: true, - searchingReleases: false, isQueryParameterModalOpen: false, - queryModalOptions: null + queryModalOptions: null, + searchType: defaultSearchType, + searchingReleases: false, + searchQuery: defaultSearchQuery || '', + searchIndexerIds: defaultIndexerIds, + searchCategories: defaultCategories, + searchLimit: 100, + searchOffset: 0, + newSearch: true }; } @@ -58,9 +55,7 @@ class SearchFooter extends Component { this.onSearchPress(); } - setTimeout(() => { - this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true }); - }); + this.props.bindShortcut('enter', this.onSearchPress, { isGlobal: true }); } componentDidUpdate(prevProps) { @@ -125,6 +120,7 @@ class SearchFooter extends Component { }; onSearchPress = () => { + const { searchLimit, searchOffset, @@ -190,13 +186,12 @@ class SearchFooter extends Component { break; default: icon = icons.SEARCH; - break; } - let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', { count: searchIndexerIds.length }); + let footerLabel = `Search ${searchIndexerIds.length === 0 ? 'all' : searchIndexerIds.length} Indexers`; if (isPopulated) { - footerLabel = selectedCount === 0 ? translate('FoundCountReleases', { itemCount }) : translate('SelectedCountOfCountReleases', { selectedCount, itemCount }); + footerLabel = selectedCount === 0 ? `Found ${itemCount} releases` : `Selected ${selectedCount} of ${itemCount} releases`; } return ( @@ -212,11 +207,7 @@ class SearchFooter extends Component { name="searchQuery" value={searchQuery} buttons={ - + @@ -265,10 +256,11 @@ class SearchFooter extends Component { />
+ { isPopulated && state.releases, - (state) => state.router.location, - (releases, location) => { + (releases) => { const { searchQuery: defaultSearchQuery, searchIndexerIds: defaultIndexerIds, searchCategories: defaultCategories, - searchType: defaultSearchType, - searchLimit: defaultSearchLimit, - searchOffset: defaultSearchOffset + searchType: defaultSearchType } = releases.defaults; - const { params } = parseUrl(location.search); - const defaultSearchQueryParams = {}; - - if (params.query && !defaultSearchQuery) { - defaultSearchQueryParams.searchQuery = params.query; - } - - if (params.indexerIds && !defaultIndexerIds.length) { - defaultSearchQueryParams.searchIndexerIds = params.indexerIds.split(',').map((id) => Number(id)).filter(Boolean); - } - - if (params.categories && !defaultCategories.length) { - defaultSearchQueryParams.searchCategories = params.categories.split(',').map((id) => Number(id)).filter(Boolean); - } - - if (params.type && defaultSearchType === 'search') { - defaultSearchQueryParams.searchType = params.type; - } - - if (params.limit && defaultSearchLimit === 100 && !isNaN(params.limit)) { - defaultSearchQueryParams.searchLimit = Number(params.limit); - } - - if (params.offset && !defaultSearchOffset && !isNaN(params.offset)) { - defaultSearchQueryParams.searchOffset = Number(params.offset); - } - return { - defaultSearchQueryParams, defaultSearchQuery, defaultIndexerIds, defaultCategories, - defaultSearchType, - defaultSearchLimit, - defaultSearchOffset + defaultSearchType }; } ); @@ -66,16 +32,6 @@ const mapDispatchToProps = { class SearchFooterConnector extends Component { - // - // Lifecycle - - componentDidMount() { - // Set defaults from query parameters - Object.entries(this.props.defaultSearchQueryParams).forEach(([name, value]) => { - this.onInputChange({ name, value }); - }); - } - // // Listeners @@ -97,7 +53,6 @@ class SearchFooterConnector extends Component { } SearchFooterConnector.propTypes = { - defaultSearchQueryParams: PropTypes.object.isRequired, setSearchDefault: PropTypes.func.isRequired }; diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js index d12635070..012dc48da 100644 --- a/frontend/src/Search/SearchIndex.js +++ b/frontend/src/Search/SearchIndex.js @@ -1,7 +1,6 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; @@ -11,9 +10,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import { align, icons, kinds, sortDirections } from 'Helpers/Props'; -import AddIndexerModal from 'Indexer/Add/AddIndexerModal'; -import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; +import { align, icons, sortDirections } from 'Helpers/Props'; import NoIndexer from 'Indexer/NoIndexer'; import * as keyCodes from 'Utilities/Constants/keyCodes'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; @@ -30,7 +27,13 @@ import SearchFooterConnector from './SearchFooterConnector'; import SearchIndexTableConnector from './Table/SearchIndexTableConnector'; import styles from './SearchIndex.css'; -const getViewComponent = (isSmallScreen) => (isSmallScreen ? SearchIndexOverviewsConnector : SearchIndexTableConnector); +function getViewComponent(isSmallScreen) { + if (isSmallScreen) { + return SearchIndexOverviewsConnector; + } + + return SearchIndexTableConnector; +} class SearchIndex extends Component { @@ -50,9 +53,7 @@ class SearchIndex extends Component { lastToggled: null, allSelected: false, allUnselected: false, - selectedState: {}, - isAddIndexerModalOpen: false, - isEditIndexerModalOpen: false + selectedState: {} }; } @@ -72,7 +73,7 @@ class SearchIndex extends Component { if (sortKey !== prevProps.sortKey || sortDirection !== prevProps.sortDirection || - hasDifferentItemsOrOrder(prevProps.items, items, 'guid') + hasDifferentItemsOrOrder(prevProps.items, items) ) { this.setJumpBarItems(); this.setSelectedState(); @@ -94,14 +95,7 @@ class SearchIndex extends Component { if (this.state.allUnselected) { return []; } - - return _.reduce(this.state.selectedState, (result, value, id) => { - if (value) { - result.push(id); - } - - return result; - }, []); + return getSelectedIds(this.state.selectedState, { parseIds: false }); }; setSelectedState() { @@ -147,7 +141,7 @@ class SearchIndex extends Component { } = this.props; // Reset if not sorting by sortTitle - if (sortKey !== 'sortTitle') { + if (sortKey !== 'title') { this.setState({ jumpBarItems: { order: [] } }); return; } @@ -155,7 +149,7 @@ class SearchIndex extends Component { const characters = _.reduce(items, (acc, item) => { let char = item.sortTitle.charAt(0); - if (!isNaN(Number(char))) { + if (!isNaN(char)) { char = '#'; } @@ -186,22 +180,6 @@ class SearchIndex extends Component { // // Listeners - onAddIndexerPress = () => { - this.setState({ isAddIndexerModalOpen: true }); - }; - - onAddIndexerModalClose = () => { - this.setState({ isAddIndexerModalOpen: false }); - }; - - onAddIndexerSelectIndexer = () => { - this.setState({ isEditIndexerModalOpen: true }); - }; - - onEditIndexerModalClose = () => { - this.setState({ isEditIndexerModalOpen: false }); - }; - onJumpBarItemPress = (jumpToCharacter) => { this.setState({ jumpToCharacter }); }; @@ -273,19 +251,17 @@ class SearchIndex extends Component { jumpToCharacter, selectedState, allSelected, - allUnselected, - isAddIndexerModalOpen, - isEditIndexerModalOpen + allUnselected } = this.state; const selectedIndexerIds = this.getSelectedIds(); const ViewComponent = getViewComponent(isSmallScreen); const isLoaded = !!(!error && isPopulated && items.length && this.scrollerRef.current); - const hasNoSearchResults = !totalItems; + const hasNoIndexer = !totalItems; return ( - + @@ -314,7 +290,7 @@ class SearchIndex extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} - isDisabled={hasNoSearchResults} + isDisabled={hasNoIndexer} onFilterSelect={onFilterSelect} /> @@ -327,17 +303,15 @@ class SearchIndex extends Component { innerClassName={styles.tableInnerContentBody} > { - isFetching && !isPopulated ? - : - null + isFetching && !isPopulated && + } { - !isFetching && !!error ? - + !isFetching && !!error && +
{getErrorMessage(error, 'Failed to load search results from API')} - : - null +
} { @@ -362,39 +336,25 @@ class SearchIndex extends Component { } { - !error && !isFetching && !hasIndexers ? + !error && !isFetching && !hasIndexers && : - null + /> } { - !error && !isFetching && isPopulated && hasIndexers && !items.length ? - : - null + !error && !isFetching && hasIndexers && !items.length && + } - - - - { - isLoaded && !!jumpBarItems.order.length ? + isLoaded && !!jumpBarItems.order.length && : - null + /> }
diff --git a/frontend/src/Search/SearchIndexConnector.js b/frontend/src/Search/SearchIndexConnector.js index 78a9866b2..e3302e73c 100644 --- a/frontend/src/Search/SearchIndexConnector.js +++ b/frontend/src/Search/SearchIndexConnector.js @@ -4,7 +4,6 @@ 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'; @@ -56,20 +55,12 @@ 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(); @@ -94,7 +85,6 @@ 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/CategoryLabel.js b/frontend/src/Search/Table/CategoryLabel.js new file mode 100644 index 000000000..80ca3a61d --- /dev/null +++ b/frontend/src/Search/Table/CategoryLabel.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Label from 'Components/Label'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import Tooltip from '../../Components/Tooltip/Tooltip'; + +function CategoryLabel({ categories }) { + const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id); + + if (categories?.length === 0) { + return ( + Unknown} + tooltip="Please report this issue to the GitHub as this shouldn't be happening" + position={tooltipPositions.LEFT} + /> + ); + } + + return ( + + { + sortedCategories.map((category) => { + return ( + + ); + }) + } + + ); +} + +CategoryLabel.defaultProps = { + categories: [] +}; + +CategoryLabel.propTypes = { + categories: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default CategoryLabel; diff --git a/frontend/src/Search/Table/CategoryLabel.tsx b/frontend/src/Search/Table/CategoryLabel.tsx deleted file mode 100644 index 4cfdeb1b2..000000000 --- a/frontend/src/Search/Table/CategoryLabel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import Label from 'Components/Label'; -import Tooltip from 'Components/Tooltip/Tooltip'; -import { kinds, tooltipPositions } from 'Helpers/Props'; -import { IndexerCategory } from 'Indexer/Indexer'; -import translate from 'Utilities/String/translate'; - -interface CategoryLabelProps { - categories: IndexerCategory[]; -} - -function CategoryLabel({ categories = [] }: CategoryLabelProps) { - if (categories?.length === 0) { - return ( - {translate('Unknown')}} - tooltip="Please report this issue to the GitHub as this shouldn't be happening" - position={tooltipPositions.LEFT} - /> - ); - } - - const sortedCategories = categories - .filter((cat) => cat.name !== undefined) - .sort((a, b) => a.id - b.id); - - return ( - - {sortedCategories.map((category) => { - return ; - })} - - ); -} - -export default CategoryLabel; diff --git a/frontend/src/Search/Table/ReleaseLinks.css b/frontend/src/Search/Table/ReleaseLinks.css deleted file mode 100644 index d37a082a1..000000000 --- a/frontend/src/Search/Table/ReleaseLinks.css +++ /dev/null @@ -1,13 +0,0 @@ -.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 deleted file mode 100644 index 9f91f93a4..000000000 --- a/frontend/src/Search/Table/ReleaseLinks.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 deleted file mode 100644 index 38260bc21..000000000 --- a/frontend/src/Search/Table/ReleaseLinks.tsx +++ /dev/null @@ -1,90 +0,0 @@ -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/SearchIndexHeader.js b/frontend/src/Search/Table/SearchIndexHeader.js index 17b79e2f7..6b91adb45 100644 --- a/frontend/src/Search/Table/SearchIndexHeader.js +++ b/frontend/src/Search/Table/SearchIndexHeader.js @@ -96,7 +96,7 @@ class SearchIndexHeader extends Component { isSortable={isSortable} {...otherProps} > - {typeof label === 'function' ? label() : label} + {label} ); }) diff --git a/frontend/src/Search/Table/SearchIndexItemConnector.js b/frontend/src/Search/Table/SearchIndexItemConnector.js index 4cc7fb20c..490214529 100644 --- a/frontend/src/Search/Table/SearchIndexItemConnector.js +++ b/frontend/src/Search/Table/SearchIndexItemConnector.js @@ -2,6 +2,7 @@ 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( @@ -36,6 +37,10 @@ function createMapStateToProps() { ); } +const mapDispatchToProps = { + dispatchExecuteCommand: executeCommand +}; + class SearchIndexItemConnector extends Component { // @@ -66,4 +71,4 @@ SearchIndexItemConnector.propTypes = { component: PropTypes.elementType.isRequired }; -export default connect(createMapStateToProps, null)(SearchIndexItemConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector); diff --git a/frontend/src/Search/Table/SearchIndexRow.css b/frontend/src/Search/Table/SearchIndexRow.css index b36ec4071..2e8282268 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css +++ b/frontend/src/Search/Table/SearchIndexRow.css @@ -59,41 +59,10 @@ margin: 0 2px; width: 22px; color: var(--textColor); - text-align: center; } .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 7552b96f9..6d625f58a 100644 --- a/frontend/src/Search/Table/SearchIndexRow.css.d.ts +++ b/frontend/src/Search/Table/SearchIndexRow.css.d.ts @@ -6,15 +6,12 @@ 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 new file mode 100644 index 000000000..1b740b5da --- /dev/null +++ b/frontend/src/Search/Table/SearchIndexRow.js @@ -0,0 +1,366 @@ +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 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, + 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 ( + + + + + + ); + } + + 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.isRequired, + 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 deleted file mode 100644 index 1136a7f64..000000000 --- a/frontend/src/Search/Table/SearchIndexRow.tsx +++ /dev/null @@ -1,395 +0,0 @@ -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/AdvancedSettingsButton.js b/frontend/src/Settings/AdvancedSettingsButton.js index 24383bb1e..b441ce28a 100644 --- a/frontend/src/Settings/AdvancedSettingsButton.js +++ b/frontend/src/Settings/AdvancedSettingsButton.js @@ -17,7 +17,7 @@ function AdvancedSettingsButton(props) { return ( + + + + + + + + } + /> + + + + + + + ); + } +} + +ApplicationSettings.propTypes = { + isTestingAll: PropTypes.bool.isRequired, + isSyncingIndexers: PropTypes.bool.isRequired, + onTestAllPress: PropTypes.func.isRequired, + onAppIndexerSyncPress: PropTypes.func.isRequired +}; + +export default ApplicationSettings; diff --git a/frontend/src/Settings/Applications/ApplicationSettings.tsx b/frontend/src/Settings/Applications/ApplicationSettings.tsx deleted file mode 100644 index 7fc4b1d7b..000000000 --- a/frontend/src/Settings/Applications/ApplicationSettings.tsx +++ /dev/null @@ -1,102 +0,0 @@ -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'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import { icons } from 'Helpers/Props'; -import AppProfilesConnector from 'Settings/Profiles/App/AppProfilesConnector'; -import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { testAllApplications } from 'Store/Actions/Settings/applications'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import translate from 'Utilities/String/translate'; -import ApplicationsConnector from './Applications/ApplicationsConnector'; -import ManageApplicationsModal from './Applications/Manage/ManageApplicationsModal'; - -function ApplicationSettings() { - const isSyncingIndexers = useSelector( - createCommandExecutingSelector(APP_INDEXER_SYNC) - ); - const isTestingAll = useSelector( - (state: AppState) => state.settings.applications.isTestingAll - ); - const dispatch = useDispatch(); - - const [isManageApplicationsOpen, setIsManageApplicationsOpen] = - useState(false); - - const onManageApplicationsPress = useCallback(() => { - setIsManageApplicationsOpen(true); - }, [setIsManageApplicationsOpen]); - - const onManageApplicationsModalClose = useCallback(() => { - setIsManageApplicationsOpen(false); - }, [setIsManageApplicationsOpen]); - - const onAppIndexerSyncPress = useCallback(() => { - dispatch( - executeCommand({ - name: APP_INDEXER_SYNC, - forceSync: true, - }) - ); - }, [dispatch]); - - const onTestAllPress = useCallback(() => { - dispatch(testAllApplications()); - }, [dispatch]); - - return ( - - - - - - - - - - - } - /> - - - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - - - - - ); -} - -export default ApplicationSettings; diff --git a/frontend/src/Settings/Applications/ApplicationSettingsConnector.js b/frontend/src/Settings/Applications/ApplicationSettingsConnector.js new file mode 100644 index 000000000..aece6e91f --- /dev/null +++ b/frontend/src/Settings/Applications/ApplicationSettingsConnector.js @@ -0,0 +1,35 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as commandNames from 'Commands/commandNames'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { testAllApplications } from 'Store/Actions/settingsActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import ApplicationSettings from './ApplicationSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.applications.isTestingAll, + createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC), + (isTestingAll, isSyncingIndexers) => { + return { + isTestingAll, + isSyncingIndexers + }; + } + ); +} + +function mapDispatchToProps(dispatch, props) { + return { + onTestAllPress() { + dispatch(testAllApplications()); + }, + onAppIndexerSyncPress() { + dispatch(executeCommand({ + name: commandNames.APP_INDEXER_SYNC + })); + } + }; +} + +export default connect(createMapStateToProps, mapDispatchToProps)(ApplicationSettings); diff --git a/frontend/src/Settings/Applications/Applications/AddApplicationItem.js b/frontend/src/Settings/Applications/Applications/AddApplicationItem.js index bae97990b..bb0053824 100644 --- a/frontend/src/Settings/Applications/Applications/AddApplicationItem.js +++ b/frontend/src/Settings/Applications/Applications/AddApplicationItem.js @@ -16,11 +16,10 @@ class AddApplicationItem extends Component { onApplicationSelect = () => { const { - implementation, - implementationName + implementation } = this.props; - this.props.onApplicationSelect({ implementation, implementationName }); + this.props.onApplicationSelect({ implementation }); }; // @@ -78,7 +77,6 @@ class AddApplicationItem extends Component { key={preset.name} name={preset.name} implementation={implementation} - implementationName={implementationName} onPress={onApplicationSelect} /> ); diff --git a/frontend/src/Settings/Applications/Applications/AddApplicationPresetMenuItem.js b/frontend/src/Settings/Applications/Applications/AddApplicationPresetMenuItem.js index d04aef4f0..9974f7132 100644 --- a/frontend/src/Settings/Applications/Applications/AddApplicationPresetMenuItem.js +++ b/frontend/src/Settings/Applications/Applications/AddApplicationPresetMenuItem.js @@ -10,14 +10,12 @@ class AddApplicationPresetMenuItem extends Component { onPress = () => { const { name, - implementation, - implementationName + implementation } = this.props; this.props.onPress({ name, - implementation, - implementationName + implementation }); }; @@ -28,7 +26,6 @@ class AddApplicationPresetMenuItem extends Component { const { name, implementation, - implementationName, ...otherProps } = this.props; @@ -46,7 +43,6 @@ class AddApplicationPresetMenuItem extends Component { AddApplicationPresetMenuItem.propTypes = { name: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Applications/Applications/Application.css b/frontend/src/Settings/Applications/Applications/Application.css index 2fde249c7..93912850e 100644 --- a/frontend/src/Settings/Applications/Applications/Application.css +++ b/frontend/src/Settings/Applications/Applications/Application.css @@ -4,11 +4,6 @@ width: 290px; } -.nameContainer { - display: flex; - justify-content: space-between; -} - .name { @add-mixin truncate; @@ -17,12 +12,6 @@ font-size: 24px; } -.externalLink { - composes: button from '~Components/Link/IconButton.css'; - - height: 36px; -} - .enabled { display: flex; flex-wrap: wrap; diff --git a/frontend/src/Settings/Applications/Applications/Application.css.d.ts b/frontend/src/Settings/Applications/Applications/Application.css.d.ts index 085b1a3c5..58a29f414 100644 --- a/frontend/src/Settings/Applications/Applications/Application.css.d.ts +++ b/frontend/src/Settings/Applications/Applications/Application.css.d.ts @@ -3,9 +3,7 @@ interface CssExports { 'application': string; 'enabled': string; - 'externalLink': string; 'name': string; - 'nameContainer': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/Applications/Applications/Application.js b/frontend/src/Settings/Applications/Applications/Application.js index 086d39ee1..728747ecf 100644 --- a/frontend/src/Settings/Applications/Applications/Application.js +++ b/frontend/src/Settings/Applications/Applications/Application.js @@ -2,10 +2,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Card from 'Components/Card'; import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { icons, kinds } from 'Helpers/Props'; +import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import EditApplicationModalConnector from './EditApplicationModalConnector'; import styles from './Application.css'; @@ -57,35 +55,17 @@ class Application extends Component { const { id, name, - enable, - syncLevel, - fields, - tags, - tagList + syncLevel } = this.props; - const applicationUrl = fields.find((field) => field.name === 'baseUrl')?.value; - return ( -
-
- {name} -
- - { - enable && applicationUrl ? - : null - } +
+ {name}
{ @@ -112,11 +92,6 @@ class Application extends Component { } - -
@@ -72,7 +71,6 @@ class Applications extends Component { ); @@ -111,7 +109,6 @@ Applications.propTypes = { isFetching: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteApplication: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js index 9f5e570c5..a984299f0 100644 --- a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js +++ b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js @@ -4,20 +4,13 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Applications from './Applications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.applications', sortByProp('name')), - createTagsSelector(), - (applications, tagList) => { - return { - ...applications, - tagList - }; - } + createSortedSectionSelector('settings.applications', sortByName), + (applications) => applications ); } diff --git a/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js b/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js index 00e30cdb7..2fdd57161 100644 --- a/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js +++ b/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js @@ -14,29 +14,13 @@ 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 './EditApplicationModalContent.css'; const syncLevelOptions = [ - { - key: 'disabled', - get value() { - return translate('Disabled'); - } - }, - { - key: 'addOnly', - get value() { - return translate('AddRemoveOnly'); - } - }, - { - key: 'fullSync', - get value() { - return translate('FullSync'); - } - } + { key: 'disabled', value: translate('Disabled') }, + { key: 'addOnly', value: translate('AddRemoveOnly') }, + { key: 'fullSync', value: translate('FullSync') } ]; function EditApplicationModalContent(props) { @@ -54,13 +38,11 @@ function EditApplicationModalContent(props) { onSavePress, onTestPress, onDeleteApplicationPress, - onAdvancedSettingsPress, ...otherProps } = props; const { id, - implementationName, name, syncLevel, tags, @@ -71,7 +53,7 @@ function EditApplicationModalContent(props) { return ( - {id ? translate('EditApplicationImplementation', { implementationName }) : translate('AddApplicationImplementation', { implementationName })} + {`${id ? translate('Edit') : translate('Add')} ${translate('Application')}`} @@ -118,10 +100,7 @@ function EditApplicationModalContent(props) { type={inputTypes.SELECT} values={syncLevelOptions} name="syncLevel" - helpTexts={[ - translate('SyncLevelAddRemove'), - translate('SyncLevelFull') - ]} + helpText={`${translate('SyncLevelAddRemove')}
${translate('SyncLevelFull')}`} {...syncLevel} onChange={onInputChange} /> @@ -133,8 +112,7 @@ function EditApplicationModalContent(props) { @@ -171,12 +149,6 @@ function EditApplicationModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -78,7 +67,6 @@ class EditApplicationModalContentConnector extends Component { onTestPress={this.onTestPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} /> ); } @@ -94,8 +82,7 @@ EditApplicationModalContentConnector.propTypes = { setApplicationFieldValue: PropTypes.func, saveApplication: PropTypes.func, testApplication: PropTypes.func, - onModalClose: PropTypes.func.isRequired, - toggleAdvancedSettings: PropTypes.func.isRequired + onModalClose: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(EditApplicationModalContentConnector); diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModal.tsx b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModal.tsx deleted file mode 100644 index 1b99f543a..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModal.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageApplicationsEditModalContent from './ManageApplicationsEditModalContent'; - -interface ManageApplicationsEditModalProps { - isOpen: boolean; - applicationIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -function ManageApplicationsEditModal(props: ManageApplicationsEditModalProps) { - const { isOpen, applicationIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageApplicationsEditModal; diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css deleted file mode 100644 index ea406894e..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css.d.ts b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css.d.ts deleted file mode 100644 index cbf2d6328..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalFooter': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx deleted file mode 100644 index 57e88a4fe..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import { ApplicationSyncLevel } from 'typings/Application'; -import translate from 'Utilities/String/translate'; -import styles from './ManageApplicationsEditModalContent.css'; - -interface SavePayload { - syncLevel?: ApplicationSyncLevel; -} - -interface ManageApplicationsEditModalContentProps { - applicationIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -const NO_CHANGE = 'noChange'; - -const syncLevelOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: ApplicationSyncLevel.Disabled, - get value() { - return translate('Disabled'); - }, - }, - { - key: ApplicationSyncLevel.AddOnly, - get value() { - return translate('AddRemoveOnly'); - }, - }, - { - key: ApplicationSyncLevel.FullSync, - get value() { - return translate('FullSync'); - }, - }, -]; - -function ManageApplicationsEditModalContent( - props: ManageApplicationsEditModalContentProps -) { - const { applicationIds, onSavePress, onModalClose } = props; - - const [syncLevel, setSyncLevel] = useState(NO_CHANGE); - - const save = useCallback(() => { - let hasChanges = false; - const payload: SavePayload = {}; - - if (syncLevel !== NO_CHANGE) { - hasChanges = true; - payload.syncLevel = syncLevel as ApplicationSyncLevel; - } - - if (hasChanges) { - onSavePress(payload); - } - - onModalClose(); - }, [syncLevel, onSavePress, onModalClose]); - - const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { - switch (name) { - case 'syncLevel': - setSyncLevel(value); - break; - default: - console.warn(`EditApplicationsModalContent Unknown Input: '${name}'`); - } - }, - [] - ); - - const selectedCount = applicationIds.length; - - return ( - - {translate('EditSelectedApplications')} - - - - {translate('SyncLevel')} - - - - - - -
- {translate('CountApplicationsSelected', { count: selectedCount })} -
- -
- - - -
-
-
- ); -} - -export default ManageApplicationsEditModalContent; diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModal.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModal.tsx deleted file mode 100644 index e0bce2138..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageApplicationsModalContent from './ManageApplicationsModalContent'; - -interface ManageApplicationsModalProps { - isOpen: boolean; - onModalClose(): void; -} - -function ManageApplicationsModal(props: ManageApplicationsModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageApplicationsModal; diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css deleted file mode 100644 index c106388ab..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.leftButtons, -.rightButtons { - display: flex; - flex: 1 0 50%; - flex-wrap: wrap; -} - -.rightButtons { - justify-content: flex-end; -} - -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: 10px; -} \ No newline at end of file diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css.d.ts b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css.d.ts deleted file mode 100644 index 7b392fff9..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'deleteButton': string; - 'leftButtons': string; - 'rightButtons': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx deleted file mode 100644 index bb81729f3..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { ApplicationAppState } from 'App/State/SettingsAppState'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -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 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'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import ManageApplicationsEditModal from './Edit/ManageApplicationsEditModal'; -import ManageApplicationsModalRow from './ManageApplicationsModalRow'; -import TagsModal from './Tags/TagsModal'; -import styles from './ManageApplicationsModalContent.css'; - -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageApplicationsModalRow ->['onSelectedChange']; - -const COLUMNS = [ - { - name: 'name', - label: () => translate('Name'), - isSortable: true, - isVisible: true, - }, - { - name: 'implementation', - label: () => translate('Implementation'), - isSortable: true, - isVisible: true, - }, - { - name: 'syncLevel', - label: () => translate('SyncLevel'), - isSortable: true, - isVisible: true, - }, - { - name: 'tags', - label: () => translate('Tags'), - isSortable: true, - isVisible: true, - }, -]; - -interface ManageApplicationsModalContentProps { - onModalClose(): void; - sortKey?: string; - sortDirection?: SortDirection; -} - -function ManageApplicationsModalContent( - props: ManageApplicationsModalContentProps -) { - const { onModalClose } = props; - - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: ApplicationAppState = useSelector( - createClientSideCollectionSelector('settings.applications') - ); - const dispatch = useDispatch(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); - const [isSavingTags, setIsSavingTags] = useState(false); - - const [selectState, setSelectState] = useSelectState(); - - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; - - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageApplicationsSort({ sortKey: value })); - }, - [dispatch] - ); - - const onDeletePress = useCallback(() => { - setIsDeleteModalOpen(true); - }, [setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteApplications({ ids: selectedIds })); - setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); - - const onSavePress = useCallback( - (payload: object) => { - setIsEditModalOpen(false); - - dispatch( - bulkEditApplications({ - ids: selectedIds, - ...payload, - }) - ); - }, - [selectedIds, dispatch] - ); - - const onTagsPress = useCallback(() => { - setIsTagsModalOpen(true); - }, [setIsTagsModalOpen]); - - const onTagsModalClose = useCallback(() => { - setIsTagsModalOpen(false); - }, [setIsTagsModalOpen]); - - const onApplyTagsPress = useCallback( - (tags: number[], applyTags: string) => { - setIsSavingTags(true); - setIsTagsModalOpen(false); - - dispatch( - bulkEditApplications({ - ids: selectedIds, - tags, - applyTags, - }) - ); - }, - [selectedIds, dispatch] - ); - - const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); - }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] - ); - - const errorMessage = getErrorMessage( - error, - 'Unable to load download clients.' - ); - const anySelected = selectedCount > 0; - - return ( - - {translate('ManageApplications')} - - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - {isPopulated && !error && !items.length && ( - {translate('NoApplicationsFound')} - )} - - {isPopulated && !!items.length && !isFetching && !isFetching ? ( - - - {items.map((item) => { - return ( - - ); - })} - -
- ) : null} -
- - -
- - {translate('Delete')} - - - - {translate('Edit')} - - - - {translate('SetTags')} - -
- - -
- - - - - - -
- ); -} - -export default ManageApplicationsModalContent; diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css deleted file mode 100644 index 8c126288c..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css +++ /dev/null @@ -1,8 +0,0 @@ -.name, -.syncLevel, -.tags, -.implementation { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - word-break: break-all; -} diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css.d.ts b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css.d.ts deleted file mode 100644 index cd3e47aae..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.css.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'implementation': string; - 'name': string; - 'syncLevel': string; - 'tags': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.tsx deleted file mode 100644 index f41997f54..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalRow.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useCallback } from 'react'; -import Label from 'Components/Label'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import Column from 'Components/Table/Column'; -import TableRow from 'Components/Table/TableRow'; -import TagListConnector from 'Components/TagListConnector'; -import { kinds } from 'Helpers/Props'; -import { ApplicationSyncLevel } from 'typings/Application'; -import { SelectStateInputProps } from 'typings/props'; -import translate from 'Utilities/String/translate'; -import styles from './ManageApplicationsModalRow.css'; - -interface ManageApplicationsModalRowProps { - id: number; - name: string; - syncLevel: string; - implementation: string; - tags: number[]; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; -} - -function ManageApplicationsModalRow(props: ManageApplicationsModalRowProps) { - const { - id, - isSelected, - name, - syncLevel, - implementation, - tags, - onSelectedChange, - } = props; - - const onSelectedChangeWrapper = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, - }); - }, - [onSelectedChange] - ); - - return ( - - - - {name} - - - {implementation} - - - - {syncLevel === ApplicationSyncLevel.AddOnly && ( - - )} - - {syncLevel === ApplicationSyncLevel.FullSync && ( - - )} - - {syncLevel === ApplicationSyncLevel.Disabled && ( - - )} - - - - - - - ); -} - -export default ManageApplicationsModalRow; diff --git a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModal.tsx b/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModal.tsx deleted file mode 100644 index 2e24d60e8..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import TagsModalContent from './TagsModalContent'; - -interface TagsModalProps { - isOpen: boolean; - ids: number[]; - onApplyTagsPress: (tags: number[], applyTags: string) => void; - onModalClose: () => void; -} - -function TagsModal(props: TagsModalProps) { - const { isOpen, onModalClose, ...otherProps } = props; - - return ( - - - - ); -} - -export default TagsModal; diff --git a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css b/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css deleted file mode 100644 index 63be9aadd..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css +++ /dev/null @@ -1,12 +0,0 @@ -.renameIcon { - margin-left: 5px; -} - -.message { - margin-top: 20px; - margin-bottom: 10px; -} - -.result { - padding-top: 4px; -} diff --git a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css.d.ts b/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css.d.ts deleted file mode 100644 index 9b4321dcc..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'message': string; - 'renameIcon': string; - 'result': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.tsx deleted file mode 100644 index 11900311e..000000000 --- a/frontend/src/Settings/Applications/Applications/Manage/Tags/TagsModalContent.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { uniq } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { ApplicationAppState } from 'App/State/SettingsAppState'; -import { Tag } from 'App/State/TagsAppState'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Label from 'Components/Label'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import Application from 'typings/Application'; -import translate from 'Utilities/String/translate'; -import styles from './TagsModalContent.css'; - -interface TagsModalContentProps { - ids: number[]; - onApplyTagsPress: (tags: number[], applyTags: string) => void; - onModalClose: () => void; -} - -function TagsModalContent(props: TagsModalContentProps) { - const { ids, onModalClose, onApplyTagsPress } = props; - - const allApplications: ApplicationAppState = useSelector( - (state: AppState) => state.settings.applications - ); - const tagList: Tag[] = useSelector(createTagsSelector()); - - const [tags, setTags] = useState([]); - const [applyTags, setApplyTags] = useState('add'); - - const applicationsTags = useMemo(() => { - const tags = ids.reduce((acc: number[], id) => { - const s = allApplications.items.find((s: Application) => s.id === id); - - if (s) { - acc.push(...s.tags); - } - - return acc; - }, []); - - return uniq(tags); - }, [ids, allApplications]); - - const onTagsChange = useCallback( - ({ value }: { value: number[] }) => { - setTags(value); - }, - [setTags] - ); - - const onApplyTagsChange = useCallback( - ({ value }: { value: string }) => { - setApplyTags(value); - }, - [setApplyTags] - ); - - const onApplyPress = useCallback(() => { - onApplyTagsPress(tags, applyTags); - }, [tags, applyTags, onApplyTagsPress]); - - const applyTagsOptions = [ - { key: 'add', value: translate('Add') }, - { key: 'remove', value: translate('Remove') }, - { key: 'replace', value: translate('Replace') }, - ]; - - return ( - - {translate('Tags')} - - -
- - {translate('Tags')} - - - - - - {translate('ApplyTags')} - - - - - - {translate('Result')} - -
- {applicationsTags.map((id) => { - const tag = tagList.find((t) => t.id === id); - - if (!tag) { - return null; - } - - const removeTag = - (applyTags === 'remove' && tags.indexOf(id) > -1) || - (applyTags === 'replace' && tags.indexOf(id) === -1); - - return ( - - ); - })} - - {(applyTags === 'add' || applyTags === 'replace') && - tags.map((id) => { - const tag = tagList.find((t) => t.id === id); - - if (!tag) { - return null; - } - - if (applicationsTags.indexOf(id) > -1) { - return null; - } - - return ( - - ); - })} -
-
-
-
- - - - - - -
- ); -} - -export default TagsModalContent; diff --git a/frontend/src/Settings/Development/DevelopmentSettings.js b/frontend/src/Settings/Development/DevelopmentSettings.js index 128055ba8..7c25e2c68 100644 --- a/frontend/src/Settings/Development/DevelopmentSettings.js +++ b/frontend/src/Settings/Development/DevelopmentSettings.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -9,7 +8,7 @@ import FormLabel from 'Components/Form/FormLabel'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; -import { inputTypes, kinds } from 'Helpers/Props'; +import { inputTypes } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; @@ -50,9 +49,9 @@ class DevelopmentSettings extends Component { { !isFetching && error && - +
{translate('UnableToLoadDevelopmentSettings')} - +
} { diff --git a/frontend/src/Settings/DownloadClients/DownloadClientSettings.js b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js index 5bd284b45..3e060aa5d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClientSettings.js +++ b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js @@ -8,7 +8,6 @@ import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector'; -import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal'; class DownloadClientSettings extends Component { @@ -22,8 +21,7 @@ class DownloadClientSettings extends Component { this.state = { isSaving: false, - hasPendingChanges: false, - isManageDownloadClientsOpen: false + hasPendingChanges: false }; } @@ -38,14 +36,6 @@ class DownloadClientSettings extends Component { this.setState(payload); }; - onManageDownloadClientsPress = () => { - this.setState({ isManageDownloadClientsOpen: true }); - }; - - onManageDownloadClientsModalClose = () => { - this.setState({ isManageDownloadClientsOpen: false }); - }; - onSavePress = () => { if (this._saveCallback) { this._saveCallback(); @@ -63,8 +53,7 @@ class DownloadClientSettings extends Component { const { isSaving, - hasPendingChanges, - isManageDownloadClientsOpen + hasPendingChanges } = this.state; return ( @@ -82,12 +71,6 @@ class DownloadClientSettings extends Component { isSpinning={isTestingAll} onPress={dispatchTestAllDownloadClients} /> - - } onSavePress={this.onSavePress} @@ -95,11 +78,6 @@ class DownloadClientSettings extends Component { - - ); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js index e79f615ea..71c51849c 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/AddCategoryModalContent.js @@ -35,7 +35,7 @@ function AddCategoryModalContent(props) { return ( - {id ? translate('EditCategory') : translate('AddCategory')} + {`${id ? 'Edit' : 'Add'} Category`} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js index 1d1f61469..6e0a25a2d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Categories/Category.js @@ -88,7 +88,7 @@ class Category extends Component { message={
- {translate('AreYouSureYouWantToDeleteCategory')} + {translate('AreYouSureYouWantToDeleteCategory', [name])}
} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js index 13b24343d..8cea557a9 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -89,7 +89,7 @@ class DownloadClient extends Component { kind={kinds.DISABLED} outline={true} > - {translate('Priority')}: {priority} + {translate('PrioritySettings', [priority])} }
@@ -105,7 +105,7 @@ class DownloadClient extends Component { isOpen={this.state.isDeleteDownloadClientModalOpen} kind={kinds.DANGER} title={translate('DeleteDownloadClient')} - message={translate('DeleteDownloadClientMessageText', { name })} + message={translate('DeleteDownloadClientMessageText', [name])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteDownloadClient} onCancel={this.onDeleteDownloadClientModalClose} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js index 51f390d4f..640d56a89 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js @@ -1,11 +1,10 @@ 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, kinds } from 'Helpers/Props'; +import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import AddDownloadClientModal from './AddDownloadClientModal'; import DownloadClient from './DownloadClient'; @@ -60,59 +59,48 @@ class DownloadClients extends Component { } = this.state; return ( -
- -
- {translate('ProwlarrDownloadClientsAlert')} -
-
- {translate('ProwlarrDownloadClientsInAppOnlyAlert')} -
-
- -
- -
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- + +
+ { + items.map((item) => { + return ( + -
- -
+ ); + }) + } - + +
+ +
+
+
- -
-
-
+ + + +
+ ); } } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index 4f6833fcb..9cba9c1cc 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 sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByProp('name')), + createSortedSectionSelector('settings.downloadClients', sortByName), (downloadClients) => downloadClients ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index c57432710..28554a31c 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -17,7 +17,6 @@ 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'; @@ -62,7 +61,6 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteDownloadClientPress, onConfirmDeleteCategory, ...otherProps @@ -86,7 +84,7 @@ class EditDownloadClientModalContent extends Component { return ( - {id ? translate('EditDownloadClientImplementation', { implementationName }) : translate('AddDownloadClientImplementation', { implementationName })} + {`${id ? translate('Edit') : translate('Add')} ${translate('DownloadClient')} - ${implementationName}`} @@ -161,7 +159,7 @@ class EditDownloadClientModalContent extends Component { } - - { - this.props.toggleAdvancedSettings(); - }; - onConfirmDeleteCategory = (id) => { this.props.deleteDownloadClientCategory({ id }); }; @@ -94,7 +81,6 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} onConfirmDeleteCategory={this.onConfirmDeleteCategory} @@ -116,7 +102,6 @@ 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/ManageDownloadClientsEditModal.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModal.tsx deleted file mode 100644 index 549a091ff..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageDownloadClientsEditModalContent from './ManageDownloadClientsEditModalContent'; - -interface ManageDownloadClientsEditModalProps { - isOpen: boolean; - downloadClientIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -function ManageDownloadClientsEditModal( - props: ManageDownloadClientsEditModalProps -) { - const { isOpen, downloadClientIds, onSavePress, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageDownloadClientsEditModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css deleted file mode 100644 index ea406894e..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.modalFooter { - composes: modalFooter from '~Components/Modal/ModalFooter.css'; - - justify-content: space-between; -} - -.selected { - font-weight: bold; -} - -@media only screen and (max-width: $breakpointExtraSmall) { - .modalFooter { - flex-direction: column; - gap: 10px; - } -} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css.d.ts b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css.d.ts deleted file mode 100644 index cbf2d6328..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'modalFooter': string; - 'selected': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx deleted file mode 100644 index d18e694c9..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ManageDownloadClientsEditModalContent.css'; - -interface SavePayload { - enable?: boolean; - priority?: number; -} - -interface ManageDownloadClientsEditModalContentProps { - downloadClientIds: number[]; - onSavePress(payload: object): void; - onModalClose(): void; -} - -const NO_CHANGE = 'noChange'; - -const enableOptions = [ - { - key: NO_CHANGE, - get value() { - return translate('NoChange'); - }, - isDisabled: true, - }, - { - key: 'enabled', - get value() { - return translate('Enabled'); - }, - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - }, - }, -]; - -function ManageDownloadClientsEditModalContent( - props: ManageDownloadClientsEditModalContentProps -) { - const { downloadClientIds, onSavePress, onModalClose } = props; - - const [enable, setEnable] = useState(NO_CHANGE); - const [priority, setPriority] = useState(null); - - const save = useCallback(() => { - let hasChanges = false; - const payload: SavePayload = {}; - - if (enable !== NO_CHANGE) { - hasChanges = true; - payload.enable = enable === 'enabled'; - } - - if (priority !== null) { - hasChanges = true; - payload.priority = priority as number; - } - - if (hasChanges) { - onSavePress(payload); - } - - onModalClose(); - }, [enable, priority, onSavePress, onModalClose]); - - const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { - switch (name) { - case 'enable': - setEnable(value); - break; - case 'priority': - setPriority(value); - break; - default: - console.warn( - `EditDownloadClientsModalContent Unknown Input: '${name}'` - ); - } - }, - [] - ); - - const selectedCount = downloadClientIds.length; - - return ( - - {translate('EditSelectedDownloadClients')} - - - - {translate('Enabled')} - - - - - - {translate('ClientPriority')} - - - - - - -
- {translate('CountDownloadClientsSelected', { count: selectedCount })} -
- -
- - - -
-
-
- ); -} - -export default ManageDownloadClientsEditModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModal.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModal.tsx deleted file mode 100644 index 0302f3544..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import ManageDownloadClientsModalContent from './ManageDownloadClientsModalContent'; - -interface ManageDownloadClientsModalProps { - isOpen: boolean; - onModalClose(): void; -} - -function ManageDownloadClientsModal(props: ManageDownloadClientsModalProps) { - const { isOpen, onModalClose } = props; - - return ( - - - - ); -} - -export default ManageDownloadClientsModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css deleted file mode 100644 index c106388ab..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css +++ /dev/null @@ -1,16 +0,0 @@ -.leftButtons, -.rightButtons { - display: flex; - flex: 1 0 50%; - flex-wrap: wrap; -} - -.rightButtons { - justify-content: flex-end; -} - -.deleteButton { - composes: button from '~Components/Link/Button.css'; - - margin-right: 10px; -} \ No newline at end of file diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css.d.ts b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css.d.ts deleted file mode 100644 index 7b392fff9..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'deleteButton': string; - 'leftButtons': string; - 'rightButtons': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx deleted file mode 100644 index fa82d61b9..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { DownloadClientAppState } from 'App/State/SettingsAppState'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -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 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'; -import getErrorMessage from 'Utilities/Object/getErrorMessage'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal'; -import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow'; -import styles from './ManageDownloadClientsModalContent.css'; - -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageDownloadClientsModalRow ->['onSelectedChange']; - -const COLUMNS = [ - { - name: 'name', - label: () => translate('Name'), - isSortable: true, - isVisible: true, - }, - { - name: 'implementation', - label: () => translate('Implementation'), - isSortable: true, - isVisible: true, - }, - { - name: 'enable', - label: () => translate('Enabled'), - isSortable: true, - isVisible: true, - }, - { - name: 'priority', - label: () => translate('ClientPriority'), - isSortable: true, - isVisible: true, - }, -]; - -interface ManageDownloadClientsModalContentProps { - onModalClose(): void; - sortKey?: string; - sortDirection?: SortDirection; -} - -function ManageDownloadClientsModalContent( - props: ManageDownloadClientsModalContentProps -) { - const { onModalClose } = props; - - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: DownloadClientAppState = useSelector( - createClientSideCollectionSelector('settings.downloadClients') - ); - const dispatch = useDispatch(); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - const [selectState, setSelectState] = useSelectState(); - - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; - - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageDownloadClientsSort({ sortKey: value })); - }, - [dispatch] - ); - - const onDeletePress = useCallback(() => { - setIsDeleteModalOpen(true); - }, [setIsDeleteModalOpen]); - - const onDeleteModalClose = useCallback(() => { - setIsDeleteModalOpen(false); - }, [setIsDeleteModalOpen]); - - const onEditPress = useCallback(() => { - setIsEditModalOpen(true); - }, [setIsEditModalOpen]); - - const onEditModalClose = useCallback(() => { - setIsEditModalOpen(false); - }, [setIsEditModalOpen]); - - const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteDownloadClients({ ids: selectedIds })); - setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); - - const onSavePress = useCallback( - (payload: object) => { - setIsEditModalOpen(false); - - dispatch( - bulkEditDownloadClients({ - ids: selectedIds, - ...payload, - }) - ); - }, - [selectedIds, dispatch] - ); - - const onSelectAllChange = useCallback( - ({ value }: SelectStateInputProps) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); - }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] - ); - - const errorMessage = getErrorMessage( - error, - 'Unable to load download clients.' - ); - const anySelected = selectedCount > 0; - - return ( - - {translate('ManageDownloadClients')} - - {isFetching ? : null} - - {error ?
{errorMessage}
: null} - - {isPopulated && !error && !items.length && ( - {translate('NoDownloadClientsFound')} - )} - - {isPopulated && !!items.length && !isFetching && !isFetching ? ( - - - {items.map((item) => { - return ( - - ); - })} - -
- ) : null} -
- - -
- - {translate('Delete')} - - - - {translate('Edit')} - -
- - -
- - - - -
- ); -} - -export default ManageDownloadClientsModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css deleted file mode 100644 index 444f376cc..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css +++ /dev/null @@ -1,8 +0,0 @@ -.name, -.enable, -.priority, -.implementation { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - word-break: break-all; -} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts deleted file mode 100644 index 6c8cd9c29..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file is automatically generated. -// Please do not change this file! -interface CssExports { - 'enable': string; - 'implementation': string; - 'name': string; - 'priority': string; -} -export const cssExports: CssExports; -export default cssExports; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx deleted file mode 100644 index 001bced52..000000000 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useCallback } from 'react'; -import Label from 'Components/Label'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import Column from 'Components/Table/Column'; -import TableRow from 'Components/Table/TableRow'; -import { kinds } from 'Helpers/Props'; -import { SelectStateInputProps } from 'typings/props'; -import translate from 'Utilities/String/translate'; -import styles from './ManageDownloadClientsModalRow.css'; - -interface ManageDownloadClientsModalRowProps { - id: number; - name: string; - enable: boolean; - priority: number; - implementation: string; - columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; -} - -function ManageDownloadClientsModalRow( - props: ManageDownloadClientsModalRowProps -) { - const { - id, - isSelected, - name, - enable, - priority, - implementation, - onSelectedChange, - } = props; - - const onSelectedChangeWrapper = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, - }); - }, - [onSelectedChange] - ); - - return ( - - - - {name} - - - {implementation} - - - - - - - {priority} - - ); -} - -export default ManageDownloadClientsModalRow; diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index 7289ed1c7..38bb08f75 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -1,7 +1,6 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ConfirmModal from 'Components/Modal/ConfirmModal'; @@ -124,9 +123,9 @@ class GeneralSettings extends Component { { !isFetching && error && - +
{translate('UnableToLoadGeneralSettings')} - +
} { @@ -156,7 +155,6 @@ class GeneralSettings extends Component { /> diff --git a/frontend/src/Settings/General/HostSettings.js b/frontend/src/Settings/General/HostSettings.js index 99255b0d5..5445808b0 100644 --- a/frontend/src/Settings/General/HostSettings.js +++ b/frontend/src/Settings/General/HostSettings.js @@ -21,7 +21,6 @@ function HostSettings(props) { port, urlBase, instanceName, - applicationUrl, enableSsl, sslPort, sslCertPath, @@ -90,21 +89,6 @@ function HostSettings(props) { /> - - {translate('ApplicationURL')} - - - - - - - {translate('LogSizeLimit')} - - - ); } LoggingSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js index 8e2597741..4b382800c 100644 --- a/frontend/src/Settings/General/SecuritySettings.js +++ b/frontend/src/Settings/General/SecuritySettings.js @@ -11,69 +11,24 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import { icons, inputTypes, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning'); + export const authenticationMethodOptions = [ - { - key: 'none', - get value() { - return translate('None'); - }, - isDisabled: true - }, - { - key: 'external', - get value() { - return translate('External'); - }, - isHidden: true - }, - { - key: 'basic', - get value() { - return translate('AuthBasic'); - } - }, - { - key: 'forms', - get value() { - return translate('AuthForm'); - } - } + { key: 'none', value: 'None', isDisabled: true }, + { key: 'external', value: 'External', isHidden: true }, + { key: 'basic', value: 'Basic (Browser Popup)' }, + { key: 'forms', value: 'Forms (Login Page)' } ]; export const authenticationRequiredOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - } + { key: 'enabled', value: 'Enabled' }, + { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' } ]; const certificateValidationOptions = [ - { - key: 'enabled', - get value() { - return translate('Enabled'); - } - }, - { - key: 'disabledForLocalAddresses', - get value() { - return translate('DisabledForLocalAddresses'); - } - }, - { - key: 'disabled', - get value() { - return translate('Disabled'); - } - } + { key: 'enabled', value: 'Enabled' }, + { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }, + { key: 'disabled', value: 'Disabled' } ]; class SecuritySettings extends Component { @@ -124,7 +79,6 @@ class SecuritySettings extends Component { authenticationRequired, username, password, - passwordConfirmation, apiKey, certificateValidation } = settings; @@ -141,7 +95,7 @@ class SecuritySettings extends Component { name="authenticationMethod" values={authenticationMethodOptions} helpText={translate('AuthenticationMethodHelpText')} - helpTextWarning={translate('AuthenticationRequiredWarning')} + helpTextWarning={authenticationRequiredWarning} onChange={onInputChange} {...authenticationMethod} /> @@ -194,21 +148,6 @@ class SecuritySettings extends Component { null } - { - authenticationEnabled ? - - {translate('PasswordConfirmation')} - - - : - null - } - {translate('ApiKey')} @@ -216,7 +155,6 @@ class SecuritySettings extends Component { type={inputTypes.TEXT} name="apiKey" readOnly={true} - helpTextWarning={translate('RestartRequiredHelpTextWarning')} buttons={[ @@ -61,58 +62,61 @@ function UpdateSettings(props) { /> -
- - {translate('Automatic')} + { + !isWindows && +
+ + {translate('Automatic')} - - + + - - {translate('Mechanism')} - - - - - { - updateMechanism.value === 'script' && - {translate('ScriptPath')} + {translate('Mechanism')} - } -
+ + { + updateMechanism.value === 'script' && + + {translate('ScriptPath')} + + + + } +
+ } ); } diff --git a/frontend/src/Settings/Indexers/IndexerProxies/AddIndexerProxyItem.js b/frontend/src/Settings/Indexers/IndexerProxies/AddIndexerProxyItem.js index 77e1e6a98..ff238c915 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/AddIndexerProxyItem.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/AddIndexerProxyItem.js @@ -16,11 +16,10 @@ class AddIndexerProxyItem extends Component { onIndexerProxySelect = () => { const { - implementation, - implementationName + implementation } = this.props; - this.props.onIndexerProxySelect({ implementation, implementationName }); + this.props.onIndexerProxySelect({ implementation }); }; // @@ -78,7 +77,6 @@ class AddIndexerProxyItem extends Component { key={preset.name} name={preset.name} implementation={implementation} - implementationName={implementationName} onPress={onIndexerProxySelect} /> ); diff --git a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js index 59e10422b..59ce4e820 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/EditIndexerProxyModalContent.js @@ -14,7 +14,6 @@ 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'; @@ -32,7 +31,6 @@ function EditIndexerProxyModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteIndexerProxyPress, ...otherProps } = props; @@ -49,7 +47,7 @@ function EditIndexerProxyModalContent(props) { return ( - {id ? translate('EditIndexerProxyImplementation', { implementationName }) : translate('AddIndexerProxyImplementation', { implementationName })} + {`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`} @@ -132,12 +130,6 @@ function EditIndexerProxyModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditIndexerProxyModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ 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 0d2acae87..9d2188a7c 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 sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import IndexerProxies from './IndexerProxies'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexerProxies', sortByProp('name')), - createSortedSectionSelector('indexers', sortByProp('name')), + createSortedSectionSelector('settings.indexerProxies', sortByName), + createSortedSectionSelector('indexers', sortByName), createTagsSelector(), (indexerProxies, indexers, tagList) => { return { diff --git a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js index dcd7e1f35..84292ae65 100644 --- a/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js +++ b/frontend/src/Settings/Indexers/IndexerProxies/IndexerProxy.js @@ -122,7 +122,7 @@ class IndexerProxy extends Component { isOpen={this.state.isDeleteIndexerProxyModalOpen} kind={kinds.DANGER} title={translate('DeleteIndexerProxy')} - message={translate('DeleteIndexerProxyMessageText', { name })} + message={translate('DeleteIndexerProxyMessageText', [name])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteIndexerProxy} onCancel={this.onDeleteIndexerProxyModalClose} diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js index a48a8f9d5..aaf784094 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js @@ -28,7 +28,7 @@ class AddNotificationModalContent extends Component { return ( - {translate('AddConnection')} + Add Notification diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index ed00d96e6..ec20ccff1 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -14,7 +14,6 @@ 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'; @@ -33,7 +32,6 @@ function EditNotificationModalContent(props) { onModalClose, onSavePress, onTestPress, - onAdvancedSettingsPress, onDeleteNotificationPress, ...otherProps } = props; @@ -50,7 +48,7 @@ function EditNotificationModalContent(props) { return ( - {id ? translate('EditConnectionImplementation', { implementationName }) : translate('AddConnectionImplementation', { implementationName })} + {`${id ? 'Edit' : 'Add'} Connection - ${implementationName}`} @@ -138,12 +136,6 @@ function EditNotificationModalContent(props) { } - - { - this.props.toggleAdvancedSettings(); - }; - // // Render @@ -76,7 +65,6 @@ class EditNotificationModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} - onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -94,7 +82,6 @@ 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/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index 4ecf33047..1331a7c8b 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import Card from 'Components/Card'; import Label from 'Components/Label'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import EditNotificationModalConnector from './EditNotificationModalConnector'; @@ -58,14 +57,10 @@ class Notification extends Component { name, onGrab, onHealthIssue, - onHealthRestored, onApplicationUpdate, supportsOnGrab, supportsOnHealthIssue, - supportsOnHealthRestored, - supportsOnApplicationUpdate, - tags, - tagList + supportsOnApplicationUpdate } = this.props; return ( @@ -79,27 +74,17 @@ class Notification extends Component {
{ - supportsOnGrab && onGrab ? + supportsOnGrab && onGrab && : - null + } { - supportsOnHealthIssue && onHealthIssue ? + supportsOnHealthIssue && onHealthIssue && : - null - } - - { - supportsOnHealthRestored && onHealthRestored ? - : - null + } { @@ -111,7 +96,7 @@ class Notification extends Component { } { - !onGrab && !onHealthIssue && !onHealthRestored && !onApplicationUpdate ? + !onGrab && !onHealthIssue && !onApplicationUpdate ?
-
- -
- { - (onHealthIssue.value || onHealthRestored.value) && + onHealthIssue.value &&
); @@ -111,7 +109,6 @@ Notifications.propTypes = { isFetching: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteNotification: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index 6351c6f8a..83ee6c697 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -4,20 +4,13 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByProp('name')), - createTagsSelector(), - (notifications, tagList) => { - return { - ...notifications, - tagList - }; - } + createSortedSectionSelector('settings.notifications', sortByName), + (notifications) => notifications ); } diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js index 213445c65..4cb83e8f6 100644 --- a/frontend/src/Settings/PendingChangesModal.js +++ b/frontend/src/Settings/PendingChangesModal.js @@ -15,17 +15,12 @@ function PendingChangesModal(props) { isOpen, onConfirm, onCancel, - bindShortcut, - unbindShortcut + bindShortcut } = props; useEffect(() => { - if (isOpen) { - bindShortcut('enter', onConfirm); - - return () => unbindShortcut('enter', onConfirm); - } - }, [bindShortcut, unbindShortcut, isOpen, onConfirm]); + bindShortcut('enter', onConfirm); + }, [bindShortcut, onConfirm]); return ( - {translate('Rss')} + {translate('RSS')} } @@ -130,7 +130,7 @@ class AppProfile extends Component { isOpen={this.state.isDeleteAppProfileModalOpen} kind={kinds.DANGER} title={translate('DeleteAppProfile')} - message={translate('DeleteAppProfileMessageText', { name })} + message={translate('AppProfileDeleteConfirm', [name])} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteAppProfile} diff --git a/frontend/src/Settings/Profiles/App/AppProfilesConnector.js b/frontend/src/Settings/Profiles/App/AppProfilesConnector.js index 02bf845df..a150655a6 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 sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import AppProfiles from './AppProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.appProfiles', sortByProp('name')), + createSortedSectionSelector('settings.appProfiles', sortByName), (appProfiles) => appProfiles ); } diff --git a/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js b/frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js index ac67c77f2..aace8e039 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('EnableAutomaticSearch')} - - - - - {translate('EnableInteractiveSearch')} @@ -125,6 +111,20 @@ class EditAppProfileModalContent extends Component { /> + + + {translate('EnableAutomaticSearch')} + + + + + {translate('MinimumSeeders')} diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index afeb3863a..607a67543 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -137,7 +137,7 @@ class Tag extends Component { isOpen={isDeleteTagModalOpen} kind={kinds.DANGER} title={translate('DeleteTag')} - message={translate('DeleteTagMessageText', { label })} + message={translate('DeleteTagMessageText', [label])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteTag} onCancel={this.onDeleteTagModalClose} diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 1f3de2034..4f311e984 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,14 +3,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions'; -import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { fetchTagDetails } from 'Store/Actions/tagActions'; import Tags from './Tags'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), + (state) => state.tags, (tags) => { const isFetching = tags.isFetching || tags.details.isFetching; const error = tags.error || tags.details.error; @@ -27,7 +25,6 @@ function createMapStateToProps() { } const mapDispatchToProps = { - dispatchFetchTags: fetchTags, dispatchFetchTagDetails: fetchTagDetails, dispatchFetchNotifications: fetchNotifications, dispatchFetchIndexerProxies: fetchIndexerProxies, @@ -41,14 +38,12 @@ class MetadatasConnector extends Component { componentDidMount() { const { - dispatchFetchTags, dispatchFetchTagDetails, dispatchFetchNotifications, dispatchFetchIndexerProxies, dispatchFetchApplications } = this.props; - dispatchFetchTags(); dispatchFetchTagDetails(); dispatchFetchNotifications(); dispatchFetchIndexerProxies(); @@ -68,7 +63,6 @@ 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 d156f4ff3..83443cd72 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -9,7 +8,7 @@ import FormLabel from 'Components/Form/FormLabel'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; -import { inputTypes, kinds } from 'Helpers/Props'; +import { inputTypes } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import themes from 'Styles/Themes'; import titleCase from 'Utilities/String/titleCase'; @@ -21,19 +20,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { 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' } + { 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' } ]; const shortDateFormatOptions = [ - { 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' } + { 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' } ]; const longDateFormatOptions = [ @@ -81,9 +80,9 @@ class UISettings extends Component { { !isFetching && error && - +
{translate('UnableToLoadUISettings')} - +
} { @@ -147,7 +146,7 @@ class UISettings extends Component { language.key === settings.uiLanguage.value) ? - settings.uiLanguage.errors : - [ - ...settings.uiLanguage.errors, - { message: translate('InvalidUILanguage') } - ]} />
diff --git a/frontend/src/Store/Actions/Creators/createBulkEditItemHandler.js b/frontend/src/Store/Actions/Creators/createBulkEditItemHandler.js deleted file mode 100644 index f174dae54..000000000 --- a/frontend/src/Store/Actions/Creators/createBulkEditItemHandler.js +++ /dev/null @@ -1,54 +0,0 @@ -import { batchActions } from 'redux-batched-actions'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { set, updateItem } from '../baseActions'; - -function createBulkEditItemHandler(section, url) { - return function(getState, payload, dispatch) { - - dispatch(set({ section, isSaving: true })); - - const ajaxOptions = { - url: `${url}`, - method: 'PUT', - data: JSON.stringify(payload), - dataType: 'json' - }; - - const promise = createAjaxRequest(ajaxOptions).request; - - promise.done((data) => { - dispatch(batchActions([ - set({ - section, - isSaving: false, - saveError: null - }), - - ...data.map((provider) => { - - const { - ...propsToUpdate - } = provider; - - return updateItem({ - id: provider.id, - section, - ...propsToUpdate - }); - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isSaving: false, - saveError: xhr - })); - }); - - return promise; - }; -} - -export default createBulkEditItemHandler; diff --git a/frontend/src/Store/Actions/Creators/createBulkRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createBulkRemoveItemHandler.js deleted file mode 100644 index 3293ff1b5..000000000 --- a/frontend/src/Store/Actions/Creators/createBulkRemoveItemHandler.js +++ /dev/null @@ -1,48 +0,0 @@ -import { batchActions } from 'redux-batched-actions'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { removeItem, set } from '../baseActions'; - -function createBulkRemoveItemHandler(section, url) { - return function(getState, payload, dispatch) { - const { - ids - } = payload; - - dispatch(set({ section, isDeleting: true })); - - const ajaxOptions = { - url: `${url}`, - method: 'DELETE', - data: JSON.stringify(payload), - dataType: 'json' - }; - - const promise = createAjaxRequest(ajaxOptions).request; - - promise.done((data) => { - dispatch(batchActions([ - set({ - section, - isDeleting: false, - deleteError: null - }), - - ...ids.map((id) => { - return removeItem({ section, id }); - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isDeleting: false, - deleteError: xhr - })); - }); - - return promise; - }; -} - -export default createBulkRemoveItemHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index f5ef10a4d..a80ee1e45 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -6,8 +6,6 @@ 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 })); @@ -27,13 +25,10 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const { selectedFilterKey, - filters + filters, + customFilters } = sectionState; - const customFilters = getState().customFilters.items.filter((customFilter) => { - return customFilter.type === section || customFilter.type === baseSection; - }); - const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -42,8 +37,7 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data, - traditional: true + data }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js index 1cccf1666..5761655d2 100644 --- a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js @@ -32,9 +32,9 @@ function createSaveProviderHandler(section, url, options = {}) { const params = { ...queryParams }; // If the user is re-saving the same provider without changes - // force it to be saved. + // force it to be saved. Only applies to editing existing providers. - if (_.isEqual(saveData, lastSaveData)) { + if (id && _.isEqual(saveData, lastSaveData)) { params.forceSave = true; } diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index e35157dbd..ca26883fb 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,11 +1,8 @@ -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) { @@ -20,25 +17,10 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - 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 testData = getProviderState(payload, getState, section); const ajaxOptions = { - url: `${url}/test?${$.param(params, true)}`, + url: `${url}/test`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -50,8 +32,6 @@ 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/appProfiles.js b/frontend/src/Store/Actions/Settings/appProfiles.js index 92a48e0b8..70f8a8961 100644 --- a/frontend/src/Store/Actions/Settings/appProfiles.js +++ b/frontend/src/Store/Actions/Settings/appProfiles.js @@ -7,7 +7,6 @@ import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/create import { createThunk } from 'Store/thunks'; import getSectionState from 'Utilities/State/getSectionState'; import updateSectionState from 'Utilities/State/updateSectionState'; -import translate from 'Utilities/String/translate'; // // Variables @@ -53,14 +52,14 @@ export default { isFetching: false, isPopulated: false, error: null, + isDeleting: false, + deleteError: null, isSchemaFetching: false, isSchemaPopulated: false, schemaError: null, schema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, items: [], pendingChanges: {} }, @@ -88,7 +87,7 @@ export default { const pendingChanges = { ...item, id: 0 }; delete pendingChanges.id; - pendingChanges.name = translate('DefaultNameCopiedProfile', { name: pendingChanges.name }); + pendingChanges.name = `${pendingChanges.name} - Copy`; newState.pendingChanges = pendingChanges; return updateSectionState(state, section, newState); diff --git a/frontend/src/Store/Actions/Settings/applications.js b/frontend/src/Store/Actions/Settings/applications.js index 53a008b0c..a670732e0 100644 --- a/frontend/src/Store/Actions/Settings/applications.js +++ b/frontend/src/Store/Actions/Settings/applications.js @@ -1,14 +1,10 @@ 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'; import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; 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'; @@ -32,10 +28,7 @@ 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 = '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'; +export const TEST_ALL_APPLICATIONS = 'indexers/testAllApplications'; // // Action Creators @@ -50,9 +43,6 @@ export const deleteApplication = createThunk(DELETE_APPLICATION); export const testApplication = createThunk(TEST_APPLICATION); 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 { @@ -87,19 +77,10 @@ export default { selectedSchema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); - } - } + pendingChanges: {} }, // @@ -114,9 +95,7 @@ export default { [DELETE_APPLICATION]: createRemoveItemHandler(section, '/applications'), [TEST_APPLICATION]: createTestProviderHandler(section, '/applications'), [CANCEL_TEST_APPLICATION]: createCancelTestProviderHandler(section), - [TEST_ALL_APPLICATIONS]: createTestAllProvidersHandler(section, '/applications'), - [BULK_EDIT_APPLICATIONS]: createBulkEditItemHandler(section, '/applications/bulk'), - [BULK_DELETE_APPLICATIONS]: createBulkRemoveItemHandler(section, '/applications/bulk') + [TEST_ALL_APPLICATIONS]: createTestAllProvidersHandler(section, '/applications') }, // @@ -128,14 +107,14 @@ export default { [SELECT_APPLICATION_SCHEMA]: (state, { payload }) => { return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = selectedSchema.implementationName; + selectedSchema.onGrab = selectedSchema.supportsOnGrab; + selectedSchema.onDownload = selectedSchema.supportsOnDownload; + selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; + selectedSchema.onRename = selectedSchema.supportsOnRename; return selectedSchema; }); - }, - - [SET_MANAGE_APPLICATIONS_SORT]: createSetClientSideCollectionSortReducer(section) - + } } }; diff --git a/frontend/src/Store/Actions/Settings/downloadClientCategories.js b/frontend/src/Store/Actions/Settings/downloadClientCategories.js index 38cce33c5..b9fb04404 100644 --- a/frontend/src/Store/Actions/Settings/downloadClientCategories.js +++ b/frontend/src/Store/Actions/Settings/downloadClientCategories.js @@ -75,8 +75,6 @@ export default { selectedSchema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, items: [], pendingChanges: {} }, diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 56784d5d0..7e9292f24 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -1,14 +1,10 @@ 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'; import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; 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,9 +30,6 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient'; export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient'; 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 @@ -51,9 +44,6 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT); export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT); 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 { @@ -88,19 +78,10 @@ export default { selectedSchema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); - } - } + pendingChanges: {} }, // @@ -139,9 +120,7 @@ export default { }, [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section), - [TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient'), - [BULK_EDIT_DOWNLOAD_CLIENTS]: createBulkEditItemHandler(section, '/downloadclient/bulk'), - [BULK_DELETE_DOWNLOAD_CLIENTS]: createBulkRemoveItemHandler(section, '/downloadclient/bulk') + [TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient') }, // @@ -153,15 +132,11 @@ export default { [SELECT_DOWNLOAD_CLIENT_SCHEMA]: (state, { payload }) => { return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = selectedSchema.implementationName; selectedSchema.enable = true; return selectedSchema; }); - }, - - [SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section) - + } } }; diff --git a/frontend/src/Store/Actions/Settings/indexerProxies.js b/frontend/src/Store/Actions/Settings/indexerProxies.js index 6c07540be..6ba5c731b 100644 --- a/frontend/src/Store/Actions/Settings/indexerProxies.js +++ b/frontend/src/Store/Actions/Settings/indexerProxies.js @@ -74,8 +74,6 @@ export default { selectedSchema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, isTesting: false, items: [], pendingChanges: {} @@ -104,8 +102,6 @@ export default { [SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => { return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = selectedSchema.implementationName; - return selectedSchema; }); } diff --git a/frontend/src/Store/Actions/Settings/notifications.js b/frontend/src/Store/Actions/Settings/notifications.js index 28346e9a6..3242cef4b 100644 --- a/frontend/src/Store/Actions/Settings/notifications.js +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -74,8 +74,6 @@ export default { selectedSchema: {}, isSaving: false, saveError: null, - isDeleting: false, - deleteError: null, isTesting: false, items: [], pendingChanges: {} @@ -104,7 +102,6 @@ export default { [SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => { return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = selectedSchema.implementationName; selectedSchema.onGrab = selectedSchema.supportsOnGrab; selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate; diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js index 2b779d1b0..a273c7292 100644 --- a/frontend/src/Store/Actions/appActions.js +++ b/frontend/src/Store/Actions/appActions.js @@ -4,7 +4,6 @@ import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getSectionState from 'Utilities/State/getSectionState'; import updateSectionState from 'Utilities/State/updateSectionState'; -import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate'; import createHandleActions from './Creators/createHandleActions'; function getDimensions(width, height) { @@ -42,12 +41,7 @@ export const defaultState = { isReconnecting: false, isDisconnected: false, isRestarting: false, - isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen, - translations: { - isFetching: true, - isPopulated: false, - error: null - } + isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen }; // @@ -59,7 +53,6 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions'; export const SET_VERSION = 'app/setVersion'; export const SET_APP_VALUE = 'app/setAppValue'; export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; -export const FETCH_TRANSLATIONS = 'app/fetchTranslations'; export const PING_SERVER = 'app/pingServer'; @@ -73,7 +66,6 @@ export const setAppValue = createAction(SET_APP_VALUE); export const showMessage = createAction(SHOW_MESSAGE); export const hideMessage = createAction(HIDE_MESSAGE); export const pingServer = createThunk(PING_SERVER); -export const fetchTranslations = createThunk(FETCH_TRANSLATIONS); // // Helpers @@ -135,17 +127,6 @@ function pingServerAfterTimeout(getState, dispatch) { export const actionHandlers = handleThunks({ [PING_SERVER]: function(getState, payload, dispatch) { pingServerAfterTimeout(getState, dispatch); - }, - [FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) { - const isFetchingComplete = await fetchAppTranslations(); - - dispatch(setAppValue({ - translations: { - isFetching: false, - isPopulated: isFetchingComplete, - error: isFetchingComplete ? null : 'Failed to load translations from API' - } - })); } }); diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index c324fe227..7ad498ba0 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 { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props'; +import { filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -30,73 +30,61 @@ export const defaultState = { columns: [ { name: 'eventType', - columnLabel: () => translate('EventType'), + columnLabel: translate('EventType'), isVisible: true, isModifiable: false }, { name: 'indexer', - label: () => translate('Indexer'), + label: translate('Indexer'), isSortable: false, isVisible: true }, { name: 'query', - label: () => translate('Query'), + label: translate('Query'), isSortable: false, isVisible: true }, { name: 'parameters', - label: () => translate('Parameters'), + label: translate('Parameters'), isSortable: false, isVisible: true }, { name: 'grabTitle', - label: () => translate('GrabTitle'), - isSortable: false, - isVisible: false - }, - { - name: 'queryType', - label: () => translate('QueryType'), + label: translate('GrabTitle'), isSortable: false, isVisible: false }, { name: 'categories', - label: () => translate('Categories'), + label: translate('Categories'), isSortable: false, isVisible: true }, { name: 'date', - label: () => translate('Date'), + label: translate('Date'), isSortable: true, isVisible: true }, { name: 'source', - label: () => translate('Source'), - isSortable: false, - isVisible: false - }, - { - name: 'host', - label: () => translate('Host'), + label: translate('Source'), isSortable: false, isVisible: false }, { name: 'elapsedTime', - label: () => translate('ElapsedTime'), + label: translate('ElapsedTime'), isSortable: false, isVisible: true }, { name: 'details', - columnLabel: () => translate('Details'), + columnLabel: translate('Details'), isVisible: true, isModifiable: false } @@ -107,12 +95,12 @@ export const defaultState = { filters: [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] }, { key: 'releaseGrabbed', - label: () => translate('Grabbed'), + label: translate('Grabbed'), filters: [ { key: 'eventType', @@ -123,7 +111,7 @@ export const defaultState = { }, { key: 'indexerRss', - label: () => translate('IndexerRss'), + label: translate('IndexerRss'), filters: [ { key: 'eventType', @@ -134,7 +122,7 @@ export const defaultState = { }, { key: 'indexerQuery', - label: () => translate('IndexerQuery'), + label: translate('IndexerQuery'), filters: [ { key: 'eventType', @@ -145,7 +133,7 @@ export const defaultState = { }, { key: 'indexerAuth', - label: () => translate('IndexerAuth'), + label: translate('IndexerAuth'), filters: [ { key: 'eventType', @@ -156,7 +144,7 @@ export const defaultState = { }, { key: 'failed', - label: () => translate('Failed'), + label: translate('Failed'), filters: [ { key: 'successful', @@ -165,27 +153,6 @@ 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/index.js b/frontend/src/Store/Actions/index.js index a25144d5a..98db37faf 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -4,7 +4,6 @@ import * as commands from './commandActions'; import * as customFilters from './customFilterActions'; import * as history from './historyActions'; import * as indexers from './indexerActions'; -import * as indexerHistory from './indexerHistoryActions'; import * as indexerIndex from './indexerIndexActions'; import * as indexerStats from './indexerStatsActions'; import * as indexerStatus from './indexerStatusActions'; @@ -29,7 +28,6 @@ export default [ releases, localization, indexers, - indexerHistory, indexerIndex, indexerStats, indexerStatus, diff --git a/frontend/src/Store/Actions/indexerActions.js b/frontend/src/Store/Actions/indexerActions.js index e11051c2f..a30d6a73a 100644 --- a/frontend/src/Store/Actions/indexerActions.js +++ b/frontend/src/Store/Actions/indexerActions.js @@ -1,15 +1,11 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; -import { filterTypePredicates, sortDirections } from 'Helpers/Props'; +import { 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'; @@ -17,10 +13,7 @@ import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import getSectionState from 'Utilities/State/getSectionState'; import updateSectionState from 'Utilities/State/updateSectionState'; 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'; // @@ -36,8 +29,6 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - isDeleting: false, - deleteError: null, selectedSchema: {}, isSaving: false, saveError: null, @@ -50,7 +41,7 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - sortKey: 'sortName', + sortKey: 'name', sortDirection: sortDirections.ASCENDING, items: [] } @@ -59,7 +50,7 @@ export const defaultState = { export const filters = [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] } ]; @@ -74,68 +65,15 @@ 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 = { - status: function({ enable, redirect }) { - let result = 0; + vipExpiration: function(item) { + const vipExpiration = + item.fields.find((field) => field.name === 'vipExpiration')?.value ?? ''; - 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; + return vipExpiration; } }; @@ -146,7 +84,6 @@ 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'; @@ -156,8 +93,6 @@ export const DELETE_INDEXER = 'indexers/deleteIndexer'; export const TEST_INDEXER = 'indexers/testIndexer'; export const CANCEL_TEST_INDEXER = 'indexers/cancelTestIndexer'; export const TEST_ALL_INDEXERS = 'indexers/testAllIndexers'; -export const BULK_EDIT_INDEXERS = 'indexers/bulkEditIndexers'; -export const BULK_DELETE_INDEXERS = 'indexers/bulkDeleteIndexers'; // // Action Creators @@ -166,7 +101,6 @@ 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); @@ -175,8 +109,6 @@ export const deleteIndexer = createThunk(DELETE_INDEXER); export const testIndexer = createThunk(TEST_INDEXER); export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); -export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS); -export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { return { @@ -229,9 +161,7 @@ export const actionHandlers = handleThunks({ [DELETE_INDEXER]: createRemoveItemHandler(section, '/indexer'), [TEST_INDEXER]: createTestProviderHandler(section, '/indexer'), [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section), - [TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer'), - [BULK_EDIT_INDEXERS]: createBulkEditItemHandler(section, '/indexer/bulk'), - [BULK_DELETE_INDEXERS]: createBulkRemoveItemHandler(section, '/indexer/bulk') + [TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer') }); // @@ -244,16 +174,12 @@ export const reducers = createHandleActions({ [SELECT_INDEXER_SCHEMA]: (state, { payload }) => { return selectSchema(state, payload, (selectedSchema) => { - selectedSchema.name = payload.name ?? payload.implementationName; - selectedSchema.implementationName = payload.implementationName; selectedSchema.enable = selectedSchema.supportsRss; return selectedSchema; }); }, - [CLEAR_INDEXER_SCHEMA]: createClearReducer(schemaSection, defaultState), - [CLONE_INDEXER]: function(state, { payload }) { const id = payload.id; const newState = getSectionState(state, section); @@ -265,20 +191,14 @@ export const reducers = createHandleActions({ delete selectedSchema.name; selectedSchema.fields = selectedSchema.fields.map((field) => { - const newField = { ...field }; - - if (newField.privacy === 'apiKey' || newField.privacy === 'password') { - newField.value = ''; - } - - return newField; + return { ...field }; }); newState.selectedSchema = selectedSchema; // Set the name in pendingChanges newState.pendingChanges = { - name: translate('DefaultNameCopiedProfile', { name: item.name }) + name: `${item.name} - Copy` }; return updateSectionState(state, section, newState); diff --git a/frontend/src/Store/Actions/indexerHistoryActions.js b/frontend/src/Store/Actions/indexerHistoryActions.js deleted file mode 100644 index 2cec678e1..000000000 --- a/frontend/src/Store/Actions/indexerHistoryActions.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createAction } from 'redux-actions'; -import { batchActions } from 'redux-batched-actions'; -import { createThunk, handleThunks } from 'Store/thunks'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { set, update } from './baseActions'; -import createHandleActions from './Creators/createHandleActions'; - -// -// Variables - -export const section = 'indexerHistory'; - -// -// State - -export const defaultState = { - isFetching: false, - isPopulated: false, - error: null, - items: [] -}; - -// -// Actions Types - -export const FETCH_INDEXER_HISTORY = 'indexerHistory/fetchIndexerHistory'; -export const CLEAR_INDEXER_HISTORY = 'indexerHistory/clearIndexerHistory'; - -// -// Action Creators - -export const fetchIndexerHistory = createThunk(FETCH_INDEXER_HISTORY); -export const clearIndexerHistory = createAction(CLEAR_INDEXER_HISTORY); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - - [FETCH_INDEXER_HISTORY]: function(getState, payload, dispatch) { - dispatch(set({ section, isFetching: true })); - - const promise = createAjaxRequest({ - url: '/history/indexer', - data: payload - }).request; - - promise.done((data) => { - dispatch(batchActions([ - update({ section, data }), - - set({ - section, - isFetching: false, - isPopulated: true, - error: null - }) - ])); - }); - - promise.fail((xhr) => { - dispatch(set({ - section, - isFetching: false, - isPopulated: false, - error: xhr - })); - }); - } -}); - -// -// Reducers - -export const reducers = createHandleActions({ - - [CLEAR_INDEXER_HISTORY]: (state) => { - return Object.assign({}, state, defaultState); - } - -}, defaultState, section); diff --git a/frontend/src/Store/Actions/indexerIndexActions.js b/frontend/src/Store/Actions/indexerIndexActions.js index a002d9b41..cb0d0b480 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -1,6 +1,10 @@ import { createAction } from 'redux-actions'; +import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; import translate from 'Utilities/String/translate'; +import { removeItem, set, updateItem } from './baseActions'; import createHandleActions from './Creators/createHandleActions'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; @@ -32,105 +36,69 @@ export const defaultState = { columns: [ { name: 'status', - columnLabel: () => translate('IndexerStatus'), + columnLabel: translate('ReleaseStatus'), isSortable: true, isVisible: true, isModifiable: false }, - { - name: 'id', - columnLabel: () => translate('IndexerId'), - label: () => translate('Id'), - isSortable: true, - isVisible: false - }, { name: 'sortName', - label: () => translate('IndexerName'), + label: translate('IndexerName'), isSortable: true, - isVisible: true + isVisible: true, + isModifiable: false }, { name: 'protocol', - label: () => translate('Protocol'), + label: translate('Protocol'), isSortable: true, isVisible: true }, { name: 'privacy', - label: () => translate('Privacy'), + label: translate('Privacy'), isSortable: true, isVisible: true }, { name: 'priority', - label: () => translate('Priority'), + label: translate('Priority'), isSortable: true, isVisible: true }, { name: 'appProfileId', - label: () => translate('SyncProfile'), + label: translate('SyncProfile'), isSortable: true, isVisible: true }, { name: 'added', - label: () => translate('Added'), + label: translate('Added'), isSortable: true, isVisible: true }, { name: 'vipExpiration', - label: () => translate('VipExpiration'), + label: translate('VipExpiration'), isSortable: true, isVisible: false }, { name: 'capabilities', - label: () => translate('Categories'), + label: translate('Categories'), isSortable: false, isVisible: true }, - { - name: 'minimumSeeders', - label: () => translate('MinimumSeeders'), - isSortable: true, - isVisible: false - }, - { - name: 'seedRatio', - label: () => translate('SeedRatio'), - isSortable: true, - isVisible: false - }, - { - name: 'seedTime', - label: () => translate('SeedTime'), - isSortable: true, - isVisible: false - }, - { - name: 'packSeedTime', - label: () => translate('PackSeedTime'), - isSortable: true, - isVisible: false - }, - { - name: 'preferMagnetUrl', - label: () => translate('PreferMagnetUrl'), - isSortable: true, - isVisible: false - }, { name: 'tags', - label: () => translate('Tags'), + label: translate('Tags'), isSortable: false, isVisible: false }, { name: 'actions', - columnLabel: () => translate('Actions'), + columnLabel: translate('Actions'), isVisible: true, isModifiable: false } @@ -148,59 +116,53 @@ export const defaultState = { filterBuilderProps: [ { name: 'name', - label: () => translate('IndexerName'), + label: translate('IndexerName'), type: filterBuilderTypes.STRING }, { name: 'enable', - label: () => translate('Enabled'), + label: translate('Enabled'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.BOOL }, { name: 'added', - label: () => translate('Added'), + label: translate('Added'), type: filterBuilderTypes.DATE, valueType: filterBuilderValueTypes.DATE }, { name: 'vipExpiration', - label: () => translate('VipExpiration'), + label: translate('VipExpiration'), type: filterBuilderTypes.DATE, valueType: filterBuilderValueTypes.DATE }, { name: 'priority', - label: () => translate('Priority'), + label: translate('Priority'), type: filterBuilderTypes.NUMBER }, { name: 'protocol', - label: () => translate('Protocol'), + label: translate('Protocol'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.PROTOCOL }, { name: 'privacy', - label: () => translate('Privacy'), + label: translate('Privacy'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.PRIVACY }, { name: 'appProfileId', - label: () => translate('SyncProfile'), + label: translate('SyncProfile'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.APP_PROFILE }, - { - name: 'categories', - label: () => translate('Categories'), - type: filterBuilderTypes.ARRAY, - valueType: filterBuilderValueTypes.CATEGORY - }, { name: 'tags', - label: () => translate('Tags'), + label: translate('Tags'), type: filterBuilderTypes.ARRAY, valueType: filterBuilderValueTypes.TAG } @@ -224,6 +186,8 @@ export const SET_INDEXER_SORT = 'indexerIndex/setIndexerSort'; export const SET_INDEXER_FILTER = 'indexerIndex/setIndexerFilter'; export const SET_INDEXER_VIEW = 'indexerIndex/setIndexerView'; export const SET_INDEXER_TABLE_OPTION = 'indexerIndex/setIndexerTableOption'; +export const SAVE_INDEXER_EDITOR = 'indexerIndex/saveIndexerEditor'; +export const BULK_DELETE_INDEXERS = 'indexerIndex/bulkDeleteIndexers'; // // Action Creators @@ -232,6 +196,89 @@ export const setIndexerSort = createAction(SET_INDEXER_SORT); export const setIndexerFilter = createAction(SET_INDEXER_FILTER); export const setIndexerView = createAction(SET_INDEXER_VIEW); export const setIndexerTableOption = createAction(SET_INDEXER_TABLE_OPTION); +export const saveIndexerEditor = createThunk(SAVE_INDEXER_EDITOR); +export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [SAVE_INDEXER_EDITOR]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/indexer/editor', + method: 'PUT', + data: JSON.stringify(payload), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(batchActions([ + ...data.map((indexer) => { + return updateItem({ + id: indexer.id, + section: 'indexers', + ...indexer + }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }, + + [BULK_DELETE_INDEXERS]: function(getState, payload, dispatch) { + dispatch(set({ + section, + isDeleting: true + })); + + const promise = createAjaxRequest({ + url: '/indexer/editor', + method: 'DELETE', + data: JSON.stringify(payload), + dataType: 'json' + }).request; + + promise.done(() => { + dispatch(batchActions([ + ...payload.indexerIds.map((id) => { + return removeItem({ section: 'indexers', id }); + }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + } +}); // // Reducers diff --git a/frontend/src/Store/Actions/indexerStatsActions.js b/frontend/src/Store/Actions/indexerStatsActions.js index 06c9586b5..e937cee93 100644 --- a/frontend/src/Store/Actions/indexerStatsActions.js +++ b/frontend/src/Store/Actions/indexerStatsActions.js @@ -3,7 +3,6 @@ import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; import translate from 'Utilities/String/translate'; import { set, update } from './baseActions'; import createHandleActions from './Creators/createHandleActions'; @@ -34,7 +33,7 @@ export const defaultState = { filters: [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] }, { @@ -56,27 +55,19 @@ export const defaultState = { filterBuilderProps: [ { - name: 'indexers', - label: () => translate('Indexers'), - type: filterBuilderTypes.CONTAINS, - valueType: filterBuilderValueTypes.INDEXER - }, - { - name: 'protocols', - label: () => translate('Protocols'), + name: 'startDate', + label: 'Start Date', type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.PROTOCOL + valueType: filterBuilderValueTypes.DATE }, { - name: 'tags', - label: () => translate('Tags'), - type: filterBuilderTypes.CONTAINS, - valueType: filterBuilderValueTypes.TAG + name: 'endDate', + label: 'End Date', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.DATE } ], - selectedFilterKey: 'all' - }; export const persistState = [ @@ -90,10 +81,6 @@ export const persistState = [ export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats'; export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter'; -function getCustomFilters(state, type) { - return state.customFilters.items.filter((customFilter) => customFilter.type === type); -} - // // Action Creators @@ -107,39 +94,23 @@ export const actionHandlers = handleThunks({ [FETCH_INDEXER_STATS]: function(getState, payload, dispatch) { const state = getState(); const indexerStats = state.indexerStats; - const customFilters = getCustomFilters(state, section); - const selectedFilters = findSelectedFilters(indexerStats.selectedFilterKey, indexerStats.filters, customFilters); const requestParams = { endDate: moment().toISOString() }; - selectedFilters.forEach((selectedFilter) => { - if (selectedFilter.key === 'indexers') { - requestParams.indexers = selectedFilter.value.join(','); - } - - if (selectedFilter.key === 'protocols') { - requestParams.protocols = selectedFilter.value.join(','); - } - - if (selectedFilter.key === 'tags') { - requestParams.tags = selectedFilter.value.join(','); - } - }); - if (indexerStats.selectedFilterKey !== 'all') { - if (indexerStats.selectedFilterKey === 'lastSeven') { - requestParams.startDate = moment().add(-7, 'days').endOf('day').toISOString(); - } + let dayCount = 7; if (indexerStats.selectedFilterKey === 'lastThirty') { - requestParams.startDate = moment().add(-30, 'days').endOf('day').toISOString(); + dayCount = 30; } if (indexerStats.selectedFilterKey === 'lastNinety') { - requestParams.startDate = moment().add(-90, 'days').endOf('day').toISOString(); + dayCount = 90; } + + requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString(); } const basesAttrs = { diff --git a/frontend/src/Store/Actions/oAuthActions.js b/frontend/src/Store/Actions/oAuthActions.js index b5b4966ac..b6b05d05e 100644 --- a/frontend/src/Store/Actions/oAuthActions.js +++ b/frontend/src/Store/Actions/oAuthActions.js @@ -60,7 +60,7 @@ function showOAuthWindow(url, payload) { responseJSON: [ { propertyName: payload.name, - errorMessage: () => translate('OAuthPopupMessage') + errorMessage: translate('OAuthPopupMessage') } ] }; diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index fd2fe441b..336c9add8 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -1,9 +1,7 @@ import $ from 'jquery'; -import React from 'react'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; -import Icon from 'Components/Icon'; -import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, icons, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getSectionState from 'Utilities/State/getSectionState'; @@ -33,18 +31,16 @@ export const defaultState = { error: null, grabError: null, items: [], - sortKey: 'age', + sortKey: 'title', sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'sortTitle', + secondarySortKey: 'title', secondarySortDirection: sortDirections.ASCENDING, defaults: { searchType: 'search', searchQuery: '', searchIndexerIds: [], - searchCategories: [], - searchLimit: 100, - searchOffset: 0 + searchCategories: [] }, columns: [ @@ -58,71 +54,67 @@ export const defaultState = { }, { name: 'protocol', - label: () => translate('Protocol'), + label: translate('Protocol'), isSortable: true, isVisible: true }, { name: 'age', - label: () => translate('Age'), + label: translate('Age'), isSortable: true, isVisible: true }, { name: 'sortTitle', - label: () => translate('Title'), + label: translate('Title'), isSortable: true, isVisible: true }, { name: 'indexer', - label: () => translate('Indexer'), + label: translate('Indexer'), isSortable: true, isVisible: true }, { name: 'size', - label: () => translate('Size'), + label: translate('Size'), isSortable: true, isVisible: true }, { name: 'files', - label: () => translate('Files'), + label: translate('Files'), isSortable: true, isVisible: false }, { name: 'grabs', - label: () => translate('Grabs'), + label: translate('Grabs'), isSortable: true, isVisible: true }, { name: 'peers', - label: () => translate('Peers'), + label: translate('Peers'), isSortable: true, isVisible: true }, { name: 'category', - label: () => translate('Category'), + label: translate('Category'), isSortable: true, isVisible: true }, { name: 'indexerFlags', - columnLabel: () => translate('IndexerFlags'), - label: React.createElement(Icon, { - name: icons.FLAG, - title: () => translate('IndexerFlags') - }), + columnLabel: 'Indexer Flags', isSortable: true, isVisible: true }, { name: 'actions', - columnLabel: () => translate('Actions'), + columnLabel: translate('Actions'), isVisible: true, isModifiable: false } @@ -164,70 +156,57 @@ export const defaultState = { filters: [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] } ], - filterPredicates: { - peers: function(item, filterValue, type) { - const predicate = filterTypePredicates[type]; - - const seeders = item.seeders || 0; - const leechers = item.leechers || 0; - const peers = seeders + leechers; - - return predicate(peers, filterValue); - } - }, - filterBuilderProps: [ { name: 'title', - label: () => translate('Title'), + label: translate('Title'), type: filterBuilderTypes.STRING }, { name: 'age', - label: () => translate('Age'), + label: translate('Age'), type: filterBuilderTypes.NUMBER }, { name: 'protocol', - label: () => translate('Protocol'), + label: translate('Protocol'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.PROTOCOL }, { name: 'indexerId', - label: () => translate('Indexer'), + label: translate('Indexer'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.INDEXER }, { name: 'size', - label: () => translate('Size'), - type: filterBuilderTypes.NUMBER, - valueType: filterBuilderValueTypes.BYTES + label: translate('Size'), + type: filterBuilderTypes.NUMBER }, { name: 'files', - label: () => translate('Files'), + label: translate('Files'), type: filterBuilderTypes.NUMBER }, { name: 'grabs', - label: () => translate('Grabs'), + label: translate('Grabs'), type: filterBuilderTypes.NUMBER }, { name: 'seeders', - label: () => translate('Seeders'), + label: translate('Seeders'), type: filterBuilderTypes.NUMBER }, { name: 'peers', - label: () => translate('Peers'), + label: translate('Peers'), type: filterBuilderTypes.NUMBER } ], @@ -369,9 +348,8 @@ export const actionHandlers = handleThunks({ promise.done((data) => { dispatch(batchActions([ - ...data.map(({ guid }) => { + ...data.map((release) => { return updateRelease({ - guid, isGrabbing: false, isGrabbed: true, grabError: null @@ -401,16 +379,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [CLEAR_RELEASES]: (state) => { - const { - sortKey, - sortDirection, - customFilters, - selectedFilterKey, - columns, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); + return Object.assign({}, state, defaultState); }, [UPDATE_RELEASE]: (state, { payload }) => { diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 75d2595cf..4910e462d 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -82,34 +82,35 @@ export const defaultState = { columns: [ { name: 'level', - columnLabel: () => translate('Level'), + columnLabel: translate('Level'), isSortable: false, isVisible: true, isModifiable: false }, { name: 'time', - label: () => translate('Time'), + label: translate('Time'), isSortable: true, isVisible: true, isModifiable: false }, { name: 'logger', - label: () => translate('Component'), + label: translate('Component'), isSortable: false, isVisible: true, isModifiable: false }, { name: 'message', - label: () => translate('Message'), + label: translate('Message'), isVisible: true, isModifiable: false }, { name: 'actions', - columnLabel: () => translate('Actions'), + columnLabel: translate('Actions'), + isSortable: true, isVisible: true, isModifiable: false } @@ -120,12 +121,12 @@ export const defaultState = { filters: [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] }, { key: 'info', - label: () => translate('Info'), + label: translate('Info'), filters: [ { key: 'level', @@ -136,7 +137,7 @@ export const defaultState = { }, { key: 'warn', - label: () => translate('Warn'), + label: translate('Warn'), filters: [ { key: 'level', @@ -147,7 +148,7 @@ export const defaultState = { }, { key: 'error', - label: () => translate('Error'), + label: translate('Error'), filters: [ { key: 'level', diff --git a/frontend/src/Store/Middleware/createPersistState.js b/frontend/src/Store/Middleware/createPersistState.js index 1840959ed..73047b5de 100644 --- a/frontend/src/Store/Middleware/createPersistState.js +++ b/frontend/src/Store/Middleware/createPersistState.js @@ -36,17 +36,10 @@ function mergeColumns(path, initialState, persistedState, computedState) { const column = initialColumns.find((i) => i.name === persistedColumn.name); if (column) { - const newColumn = {}; - - // We can't use a spread operator or Object.assign to clone the column - // or any accessors are lost and can break translations. - for (const prop of Object.keys(column)) { - Object.defineProperty(newColumn, prop, Object.getOwnPropertyDescriptor(column, prop)); - } - - newColumn.isVisible = persistedColumn.isVisible; - - columns.push(newColumn); + columns.push({ + ...column, + isVisible: persistedColumn.isVisible + }); } }); diff --git a/frontend/src/Store/Selectors/createAllIndexersSelector.ts b/frontend/src/Store/Selectors/createAllIndexersSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createAllIndexersSelector.ts rename to frontend/src/Store/Selectors/createAllIndexersSelector.js index 76641025f..178c54eed 100644 --- a/frontend/src/Store/Selectors/createAllIndexersSelector.ts +++ b/frontend/src/Store/Selectors/createAllIndexersSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createAllIndexersSelector() { return createSelector( - (state: AppState) => state.indexers, + (state) => state.indexers, (indexers) => { return indexers.items; } diff --git a/frontend/src/Store/Selectors/createAppProfileSelector.js b/frontend/src/Store/Selectors/createAppProfileSelector.js new file mode 100644 index 000000000..42452ccfd --- /dev/null +++ b/frontend/src/Store/Selectors/createAppProfileSelector.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +function createAppProfileSelector() { + return createSelector( + (state, { appProfileId }) => appProfileId, + (state) => state.settings.appProfiles.items, + (appProfileId, appProfiles) => { + return appProfiles.find((profile) => { + return profile.id === appProfileId; + }); + } + ); +} + +export default createAppProfileSelector; diff --git a/frontend/src/Store/Selectors/createAppProfileSelector.ts b/frontend/src/Store/Selectors/createAppProfileSelector.ts deleted file mode 100644 index b26ab71a4..000000000 --- a/frontend/src/Store/Selectors/createAppProfileSelector.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -function createAppProfileSelector() { - return createSelector( - (_: AppState, { appProfileId }: { appProfileId: number }) => appProfileId, - (state: AppState) => state.settings.appProfiles.items, - (appProfileId, appProfiles) => { - return appProfiles.find((profile) => profile.id === appProfileId); - } - ); -} - -export default createAppProfileSelector; diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index 1bac14f08..ae1031dca 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -108,7 +108,7 @@ function sort(items, state) { return _.orderBy(items, clauses, orders); } -export function createCustomFiltersSelector(type, alternateType) { +function createCustomFiltersSelector(type, alternateType) { return createSelector( (state) => state.customFilters.items, (customFilters) => { diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.js similarity index 50% rename from frontend/src/Store/Selectors/createCommandExecutingSelector.ts rename to frontend/src/Store/Selectors/createCommandExecutingSelector.js index 6a80e172b..6037d5820 100644 --- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js @@ -2,10 +2,13 @@ import { createSelector } from 'reselect'; import { isCommandExecuting } from 'Utilities/Command'; import createCommandSelector from './createCommandSelector'; -function createCommandExecutingSelector(name: string, contraints = {}) { - return createSelector(createCommandSelector(name, contraints), (command) => { - return isCommandExecuting(command); - }); +function createCommandExecutingSelector(name, contraints = {}) { + return createSelector( + createCommandSelector(name, contraints), + (command) => { + return isCommandExecuting(command); + } + ); } export default createCommandExecutingSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js new file mode 100644 index 000000000..709dfebaf --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandSelector.js @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandSelector(name, contraints = {}) { + return createSelector( + createCommandsSelector(), + (commands) => { + return findCommand(commands, { name, ...contraints }); + } + ); +} + +export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.ts b/frontend/src/Store/Selectors/createCommandSelector.ts deleted file mode 100644 index cced7b186..000000000 --- a/frontend/src/Store/Selectors/createCommandSelector.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSelector } from 'reselect'; -import { findCommand } from 'Utilities/Command'; -import createCommandsSelector from './createCommandsSelector'; - -function createCommandSelector(name: string, contraints = {}) { - return createSelector(createCommandsSelector(), (commands) => { - return findCommand(commands, { name, ...contraints }); - }); -} - -export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandsSelector.ts b/frontend/src/Store/Selectors/createCommandsSelector.js similarity index 71% rename from frontend/src/Store/Selectors/createCommandsSelector.ts rename to frontend/src/Store/Selectors/createCommandsSelector.js index 2dd5d24a2..7b9edffd9 100644 --- a/frontend/src/Store/Selectors/createCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createCommandsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createCommandsSelector() { return createSelector( - (state: AppState) => state.commands, + (state) => state.commands, (commands) => { return commands.items; } diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.js b/frontend/src/Store/Selectors/createDeepEqualSelector.js new file mode 100644 index 000000000..85562f28b --- /dev/null +++ b/frontend/src/Store/Selectors/createDeepEqualSelector.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; +import { createSelectorCreator, defaultMemoize } from 'reselect'; + +const createDeepEqualSelector = createSelectorCreator( + defaultMemoize, + _.isEqual +); + +export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.ts b/frontend/src/Store/Selectors/createDeepEqualSelector.ts deleted file mode 100644 index 9d4a63d2e..000000000 --- a/frontend/src/Store/Selectors/createDeepEqualSelector.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { isEqual } from 'lodash'; -import { createSelectorCreator, defaultMemoize } from 'reselect'; - -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual); - -export default createDeepEqualSelector; diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.ts b/frontend/src/Store/Selectors/createDimensionsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createDimensionsSelector.ts rename to frontend/src/Store/Selectors/createDimensionsSelector.js index b9602cb02..ce26b2e2c 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.ts +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createDimensionsSelector() { return createSelector( - (state: AppState) => state.app.dimensions, + (state) => state.app.dimensions, (dimensions) => { return dimensions; } diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts deleted file mode 100644 index 3a581587b..000000000 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/createExecutingCommandsSelector.ts b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js similarity index 78% rename from frontend/src/Store/Selectors/createExecutingCommandsSelector.ts rename to frontend/src/Store/Selectors/createExecutingCommandsSelector.js index dd16571fc..266865a8a 100644 --- a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts +++ b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import { isCommandExecuting } from 'Utilities/Command'; function createExecutingCommandsSelector() { return createSelector( - (state: AppState) => state.commands.items, + (state) => state.commands.items, (commands) => { return commands.filter((command) => isCommandExecuting(command)); } diff --git a/frontend/src/Store/Selectors/createExistingIndexerSelector.ts b/frontend/src/Store/Selectors/createExistingIndexerSelector.js similarity index 59% rename from frontend/src/Store/Selectors/createExistingIndexerSelector.ts rename to frontend/src/Store/Selectors/createExistingIndexerSelector.js index df98ab8d5..af16973b7 100644 --- a/frontend/src/Store/Selectors/createExistingIndexerSelector.ts +++ b/frontend/src/Store/Selectors/createExistingIndexerSelector.js @@ -1,15 +1,13 @@ -import { some } from 'lodash'; +import _ from 'lodash'; import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; import createAllIndexersSelector from './createAllIndexersSelector'; function createExistingIndexerSelector() { return createSelector( - (_: AppState, { definitionName }: { definitionName: string }) => - definitionName, + (state, { definitionName }) => definitionName, createAllIndexersSelector(), (definitionName, indexers) => { - return some(indexers, { definitionName }); + return _.some(indexers, { definitionName }); } ); } diff --git a/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js b/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js new file mode 100644 index 000000000..683f0419b --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerAppProfileSelector.js @@ -0,0 +1,16 @@ +import { createSelector } from 'reselect'; +import createIndexerSelector from './createIndexerSelector'; + +function createIndexerAppProfileSelector(indexerId) { + return createSelector( + (state) => state.settings.appProfiles.items, + createIndexerSelector(indexerId), + (appProfiles, indexer = {}) => { + return appProfiles.find((profile) => { + return profile.id === indexer.appProfileId; + }); + } + ); +} + +export default createIndexerAppProfileSelector; diff --git a/frontend/src/Store/Selectors/createIndexerAppProfileSelector.ts b/frontend/src/Store/Selectors/createIndexerAppProfileSelector.ts deleted file mode 100644 index ea95a9443..000000000 --- a/frontend/src/Store/Selectors/createIndexerAppProfileSelector.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import Indexer from 'Indexer/Indexer'; -import { createIndexerSelectorForHook } from './createIndexerSelector'; - -function createIndexerAppProfileSelector(indexerId: number) { - return createSelector( - (state: AppState) => state.settings.appProfiles.items, - createIndexerSelectorForHook(indexerId), - (appProfiles, indexer = {} as Indexer) => { - return appProfiles.find((profile) => profile.id === indexer.appProfileId); - } - ); -} - -export default createIndexerAppProfileSelector; diff --git a/frontend/src/Store/Selectors/createIndexerClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createIndexerClientSideCollectionItemsSelector.js index c0edaa6dd..931bddf23 100644 --- a/frontend/src/Store/Selectors/createIndexerClientSideCollectionItemsSelector.js +++ b/frontend/src/Store/Selectors/createIndexerClientSideCollectionItemsSelector.js @@ -9,12 +9,12 @@ function createUnoptimizedSelector(uiSection) { const items = indexers.items.map((s) => { const { id, - sortName + name } = s; return { id, - sortName + sortTitle: name }; }); @@ -38,7 +38,7 @@ const createMovieEqualSelector = createSelectorCreator( function createIndexerClientSideCollectionItemsSelector(uiSection) { return createMovieEqualSelector( createUnoptimizedSelector(uiSection), - (indexers) => indexers + (movies) => movies ); } diff --git a/frontend/src/Store/Selectors/createIndexerSelector.js b/frontend/src/Store/Selectors/createIndexerSelector.js new file mode 100644 index 000000000..220f9b15e --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerSelector.js @@ -0,0 +1,24 @@ +import { createSelector } from 'reselect'; + +function createIndexerSelector(id) { + if (id == null) { + return createSelector( + (state, { indexerId }) => indexerId, + (state) => state.indexers.itemMap, + (state) => state.indexers.items, + (indexerId, itemMap, allIndexers) => { + return allIndexers[itemMap[indexerId]]; + } + ); + } + + return createSelector( + (state) => state.indexers.itemMap, + (state) => state.indexers.items, + (itemMap, allIndexers) => { + return allIndexers[itemMap[id]]; + } + ); +} + +export default createIndexerSelector; diff --git a/frontend/src/Store/Selectors/createIndexerSelector.ts b/frontend/src/Store/Selectors/createIndexerSelector.ts deleted file mode 100644 index 7227d18a6..000000000 --- a/frontend/src/Store/Selectors/createIndexerSelector.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -export function createIndexerSelectorForHook(indexerId: number) { - return createSelector( - (state: AppState) => state.indexers.itemMap, - (state: AppState) => state.indexers.items, - (itemMap, allIndexers) => { - return indexerId ? allIndexers[itemMap[indexerId]] : undefined; - } - ); -} - -function createIndexerSelector() { - return createSelector( - (_: AppState, { indexerId }: { indexerId: number }) => indexerId, - (state: AppState) => state.indexers.itemMap, - (state: AppState) => state.indexers.items, - (indexerId, itemMap, allIndexers) => { - return allIndexers[itemMap[indexerId]]; - } - ); -} - -export default createIndexerSelector; diff --git a/frontend/src/Store/Selectors/createIndexerStatusSelector.js b/frontend/src/Store/Selectors/createIndexerStatusSelector.js new file mode 100644 index 000000000..1912ea1a0 --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerStatusSelector.js @@ -0,0 +1,13 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createIndexerStatusSelector(indexerId) { + return createSelector( + (state) => state.indexerStatus.items, + (indexerStatus) => { + return _.find(indexerStatus, { indexerId }); + } + ); +} + +export default createIndexerStatusSelector; diff --git a/frontend/src/Store/Selectors/createIndexerStatusSelector.ts b/frontend/src/Store/Selectors/createIndexerStatusSelector.ts deleted file mode 100644 index 035dfc3c4..000000000 --- a/frontend/src/Store/Selectors/createIndexerStatusSelector.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { find } from 'lodash'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import { IndexerStatus } from 'Indexer/Indexer'; - -function createIndexerStatusSelector(indexerId: number) { - return createSelector( - (state: AppState) => state.indexerStatus.items, - (indexerStatus) => { - return find(indexerStatus, { indexerId }) as IndexerStatus | undefined; - } - ); -} - -export default createIndexerStatusSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js new file mode 100644 index 000000000..807bf4673 --- /dev/null +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllIndexersSelector from './createAllIndexersSelector'; + +function createProfileInUseSelector(profileProp) { + return createSelector( + (state, { id }) => id, + (state) => state.settings.appProfiles.items, + createAllIndexersSelector(), + (id, profiles, indexers) => { + if (!id) { + return false; + } + + if (_.some(indexers, { [profileProp]: id }) || profiles.length <= 1) { + return true; + } + + return false; + } + ); +} + +export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.ts b/frontend/src/Store/Selectors/createProfileInUseSelector.ts deleted file mode 100644 index 8137db693..000000000 --- a/frontend/src/Store/Selectors/createProfileInUseSelector.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { some } from 'lodash'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import createAllIndexersSelector from './createAllIndexersSelector'; - -function createProfileInUseSelector(profileProp: string) { - return createSelector( - (_: AppState, { id }: { id: number }) => id, - (state: AppState) => state.settings.appProfiles.items, - createAllIndexersSelector(), - (id, profiles, indexers) => { - if (!id) { - return false; - } - - return some(indexers, { [profileProp]: id }) || profiles.length <= 1; - } - ); -} - -export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createReleaseClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createReleaseClientSideCollectionItemsSelector.js index 4bc195aa5..c76ba4236 100644 --- a/frontend/src/Store/Selectors/createReleaseClientSideCollectionItemsSelector.js +++ b/frontend/src/Store/Selectors/createReleaseClientSideCollectionItemsSelector.js @@ -9,13 +9,13 @@ function createUnoptimizedSelector(uiSection) { const items = releases.items.map((s) => { const { guid, - sortTitle, + title, indexerId } = s; return { guid, - sortTitle, + sortTitle: title, indexerId }; }); @@ -40,7 +40,7 @@ const createMovieEqualSelector = createSelectorCreator( function createReleaseClientSideCollectionItemsSelector(uiSection) { return createMovieEqualSelector( createUnoptimizedSelector(uiSection), - (releases) => releases + (movies) => movies ); } diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts rename to frontend/src/Store/Selectors/createSortedSectionSelector.js index abee01f75..331d890c9 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js @@ -1,18 +1,14 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector( - section: string, - comparer: (a: T, b: T) => number -) { +function createSortedSectionSelector(section, comparer) { 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/Store/Selectors/createSystemStatusSelector.ts b/frontend/src/Store/Selectors/createSystemStatusSelector.js similarity index 70% rename from frontend/src/Store/Selectors/createSystemStatusSelector.ts rename to frontend/src/Store/Selectors/createSystemStatusSelector.js index f5e276069..df586bbb9 100644 --- a/frontend/src/Store/Selectors/createSystemStatusSelector.ts +++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createSystemStatusSelector() { return createSelector( - (state: AppState) => state.system.status, + (state) => state.system.status, (status) => { return status.item; } diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.ts b/frontend/src/Store/Selectors/createTagDetailsSelector.js similarity index 62% rename from frontend/src/Store/Selectors/createTagDetailsSelector.ts rename to frontend/src/Store/Selectors/createTagDetailsSelector.js index 2a271cafe..dd178944c 100644 --- a/frontend/src/Store/Selectors/createTagDetailsSelector.ts +++ b/frontend/src/Store/Selectors/createTagDetailsSelector.js @@ -1,10 +1,9 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagDetailsSelector() { return createSelector( - (_: AppState, { id }: { id: number }) => id, - (state: AppState) => state.tags.details.items, + (state, { id }) => id, + (state) => state.tags.details.items, (id, tagDetails) => { return tagDetails.find((t) => t.id === id); } diff --git a/frontend/src/Store/Selectors/createTagsSelector.ts b/frontend/src/Store/Selectors/createTagsSelector.js similarity index 68% rename from frontend/src/Store/Selectors/createTagsSelector.ts rename to frontend/src/Store/Selectors/createTagsSelector.js index f653ff6e3..fbfd91cdb 100644 --- a/frontend/src/Store/Selectors/createTagsSelector.ts +++ b/frontend/src/Store/Selectors/createTagsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createTagsSelector() { return createSelector( - (state: AppState) => state.tags.items, + (state) => state.tags.items, (tags) => { return tags; } diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.ts b/frontend/src/Store/Selectors/createUISettingsSelector.js similarity index 69% rename from frontend/src/Store/Selectors/createUISettingsSelector.ts rename to frontend/src/Store/Selectors/createUISettingsSelector.js index ff539679b..b256d0e98 100644 --- a/frontend/src/Store/Selectors/createUISettingsSelector.ts +++ b/frontend/src/Store/Selectors/createUISettingsSelector.js @@ -1,9 +1,8 @@ import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; function createUISettingsSelector() { return createSelector( - (state: AppState) => state.settings.ui, + (state) => state.settings.ui, (ui) => { return ui.item; } diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js new file mode 100644 index 000000000..6aeed381f --- /dev/null +++ b/frontend/src/Store/scrollPositions.js @@ -0,0 +1,5 @@ +const scrollPositions = { + indexerIndex: 0 +}; + +export default scrollPositions; diff --git a/frontend/src/Store/scrollPositions.ts b/frontend/src/Store/scrollPositions.ts deleted file mode 100644 index 48fc68535..000000000 --- a/frontend/src/Store/scrollPositions.ts +++ /dev/null @@ -1,5 +0,0 @@ -const scrollPositions: Record = { - indexerIndex: 0, -}; - -export default scrollPositions; diff --git a/frontend/src/Store/thunks.js b/frontend/src/Store/thunks.js new file mode 100644 index 000000000..ebcf10917 --- /dev/null +++ b/frontend/src/Store/thunks.js @@ -0,0 +1,28 @@ +const thunks = {}; + +function identity(payload) { + return payload; +} + +export function createThunk(type, identityFunction = identity) { + return function(payload = {}) { + return function(dispatch, getState) { + const thunk = thunks[type]; + + if (thunk) { + return thunk(getState, identityFunction(payload), dispatch); + } + + throw Error(`Thunk handler has not been registered for ${type}`); + }; + }; +} + +export function handleThunks(handlers) { + const types = Object.keys(handlers); + + types.forEach((type) => { + thunks[type] = handlers[type]; + }); +} + diff --git a/frontend/src/Store/thunks.ts b/frontend/src/Store/thunks.ts deleted file mode 100644 index fd277211e..000000000 --- a/frontend/src/Store/thunks.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Dispatch } from 'redux'; -import AppState from 'App/State/AppState'; - -type GetState = () => AppState; -type Thunk = ( - getState: GetState, - identityFn: never, - dispatch: Dispatch -) => unknown; - -const thunks: Record = {}; - -function identity(payload: T): TResult { - return payload as unknown as TResult; -} - -export function createThunk(type: string, identityFunction = identity) { - return function (payload?: T) { - return function (dispatch: Dispatch, getState: GetState) { - const thunk = thunks[type]; - - if (thunk) { - const finalPayload = payload ?? {}; - - return thunk(getState, identityFunction(finalPayload), dispatch); - } - - throw Error(`Thunk handler has not been registered for ${type}`); - }; - }; -} - -export function handleThunks(handlers: Record) { - const types = Object.keys(handlers); - - types.forEach((type) => { - thunks[type] = handlers[type]; - }); -} diff --git a/frontend/src/Styles/Mixins/scroller.css b/frontend/src/Styles/Mixins/scroller.css index 29b2016b9..09d26d083 100644 --- a/frontend/src/Styles/Mixins/scroller.css +++ b/frontend/src/Styles/Mixins/scroller.css @@ -1,7 +1,4 @@ @define-mixin scrollbar { - scrollbar-color: var(--scrollbarBackgroundColor) transparent; - scrollbar-width: thin; - &::-webkit-scrollbar { width: 10px; height: 10px; diff --git a/frontend/src/Styles/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index a7cbb6de0..b4e0043b8 100644 --- a/frontend/src/Styles/Themes/dark.js +++ b/frontend/src/Styles/Themes/dark.js @@ -37,8 +37,8 @@ module.exports = { // Links defaultLinkHoverColor: '#fff', - linkColor: '#5d9cec', - linkHoverColor: '#5d9cec', + linkColor: '#rgb(230, 96, 0)', + linkHoverColor: '#rgb(230, 96, 0, .8)', // Header pageHeaderBackgroundColor: '#2a2a2a', @@ -74,9 +74,9 @@ module.exports = { defaultButtonTextColor: '#eee', defaultButtonBackgroundColor: '#333', - defaultBorderColor: '#393f45', + defaultBorderColor: '#eaeaea', defaultHoverBackgroundColor: '#444', - defaultHoverBorderColor: '#5a6265', + defaultHoverBorderColor: '#d6d6d6', primaryBackgroundColor: '#5d9cec', primaryBorderColor: '#5899eb', @@ -162,7 +162,7 @@ module.exports = { inputHoverBackgroundColor: 'rgba(255, 255, 255, 0.20)', inputSelectedBackgroundColor: 'rgba(255, 255, 255, 0.05)', advancedFormLabelColor: '#ff902b', - disabledCheckInputColor: '#999', + disabledCheckInputColor: '#ddd', disabledInputColor: '#808080', // @@ -187,8 +187,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'].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(',') + 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'] }; diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js index 4dec39164..d93c5dd8c 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 f88070a0f..5ff84460c 100644 --- a/frontend/src/Styles/Themes/light.js +++ b/frontend/src/Styles/Themes/light.js @@ -187,8 +187,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'].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(',') + 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'] }; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js index def48f28e..3b0077c5a 100644 --- a/frontend/src/Styles/Variables/fonts.js +++ b/frontend/src/Styles/Variables/fonts.js @@ -2,6 +2,7 @@ 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/Styles/Variables/zIndexes.js b/frontend/src/Styles/Variables/zIndexes.js index 4d10253a7..986ceb548 100644 --- a/frontend/src/Styles/Variables/zIndexes.js +++ b/frontend/src/Styles/Variables/zIndexes.js @@ -1,5 +1,4 @@ module.exports = { - pageJumpBarZIndex: 10, modalZIndex: 1000, popperZIndex: 2000 }; diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index 39f7f1123..089f6bcb9 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -4,7 +4,7 @@ import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import ConfirmModal from 'Components/Modal/ConfirmModal'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import { icons, kinds } from 'Helpers/Props'; @@ -110,13 +110,12 @@ class BackupRow extends Component { {formatBytes(size)} - @@ -139,9 +138,7 @@ 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 ede2f97f6..7a5e399d0 100644 --- a/frontend/src/System/Backup/Backups.js +++ b/frontend/src/System/Backup/Backups.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; @@ -9,7 +8,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import { icons, kinds } from 'Helpers/Props'; +import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import BackupRow from './BackupRow'; import RestoreBackupModalConnector from './RestoreBackupModalConnector'; @@ -21,17 +20,17 @@ const columns = [ }, { name: 'name', - label: () => translate('Name'), + label: translate('Name'), isVisible: true }, { name: 'size', - label: () => translate('Size'), + label: translate('Size'), isVisible: true }, { name: 'time', - label: () => translate('Time'), + label: translate('Time'), isVisible: true }, { @@ -108,16 +107,16 @@ class Backups extends Component { { !isFetching && !!error && - - {translate('BackupsLoadError')} - +
+ {translate('UnableToLoadBackups')} +
} { noBackups && - +
{translate('NoBackupsAreAvailable')} - +
} { diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.js b/frontend/src/System/Backup/RestoreBackupModalContent.js index 9b5daa9f4..150c46ad6 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 translate('ErrorRestoringBackup'); + return 'Error restoring backup'; } return error.responseJSON.message; @@ -146,9 +146,7 @@ class RestoreBackupModalContent extends Component { { - !!id && translate('WouldYouLikeToRestoreBackup', { - name - }) + !!id && `Would you like to restore the backup '${name}'?` } { @@ -205,7 +203,7 @@ class RestoreBackupModalContent extends Component {
- {translate('RestartReloadNote')} + Note: Prowlarr will automatically restart and reload the UI during the restore process.
+ } + > + { + 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, + isTestingAllIndexers: PropTypes.bool.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 deleted file mode 100644 index e0636961b..000000000 --- a/frontend/src/System/Status/Health/Health.tsx +++ /dev/null @@ -1,191 +0,0 @@ -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 new file mode 100644 index 000000000..885faa424 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthConnector.js @@ -0,0 +1,66 @@ +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 { 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.indexers.isTestingAll, + (items, health, isTestingAllIndexers) => { + const { + isFetching, + isPopulated + } = health; + + return { + isFetching, + isPopulated, + items, + isTestingAllIndexers + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchHealth: fetchHealth, + 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 deleted file mode 100644 index b7a90c783..000000000 --- a/frontend/src/System/Status/Health/HealthItemLink.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index b12fd3ebb..000000000 --- a/frontend/src/System/Status/Health/HealthStatus.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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 new file mode 100644 index 000000000..e609dd712 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatusConnector.js @@ -0,0 +1,81 @@ +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 deleted file mode 100644 index f38e3fe88..000000000 --- a/frontend/src/System/Status/Health/createHealthSelector.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 new file mode 100644 index 000000000..dfb23a996 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -0,0 +1,58 @@ +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 deleted file mode 100644 index 928449aed..000000000 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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 new file mode 100644 index 000000000..46e2d0951 --- /dev/null +++ b/frontend/src/System/Status/Status.js @@ -0,0 +1,30 @@ +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 deleted file mode 100644 index 6ae088160..000000000 --- a/frontend/src/System/Status/Status.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 6e38929c9..034804711 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css @@ -10,6 +10,15 @@ 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 2c6010533..3bc00b738 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts @@ -2,12 +2,14 @@ // 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 new file mode 100644 index 000000000..917bfa11a --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js @@ -0,0 +1,279 @@ +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 deleted file mode 100644 index 4511bcbf4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx +++ /dev/null @@ -1,238 +0,0 @@ -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 new file mode 100644 index 000000000..f55ab985a --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js @@ -0,0 +1,31 @@ +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 deleted file mode 100644 index 41acb33f8..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css +++ /dev/null @@ -1,8 +0,0 @@ -.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 deleted file mode 100644 index fc9081492..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index 601a57242..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 new file mode 100644 index 000000000..5dc901ae4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.js @@ -0,0 +1,90 @@ +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 deleted file mode 100644 index e79deed7c..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 new file mode 100644 index 000000000..5fa4d9ead --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000..acb8c8d36 --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js @@ -0,0 +1,203 @@ +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 deleted file mode 100644 index 3a3cd02de..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx +++ /dev/null @@ -1,170 +0,0 @@ -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 new file mode 100644 index 000000000..dae790d68 --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js @@ -0,0 +1,92 @@ +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 new file mode 100644 index 000000000..8dbe5c08b --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js @@ -0,0 +1,85 @@ +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 deleted file mode 100644 index fcf5764bb..000000000 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 new file mode 100644 index 000000000..8f418d3bb --- /dev/null +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js @@ -0,0 +1,46 @@ +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.tsx b/frontend/src/System/Tasks/Tasks.js similarity index 63% rename from frontend/src/System/Tasks/Tasks.tsx rename to frontend/src/System/Tasks/Tasks.js index 26473d7ba..032dbede8 100644 --- a/frontend/src/System/Tasks/Tasks.tsx +++ b/frontend/src/System/Tasks/Tasks.js @@ -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 QueuedTasks from './Queued/QueuedTasks'; -import ScheduledTasks from './Scheduled/ScheduledTasks'; +import QueuedTasksConnector from './Queued/QueuedTasksConnector'; +import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { return ( - - + + ); diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..9d6b9decc --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,50 @@ +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 deleted file mode 100644 index 460814cbe..000000000 --- a/frontend/src/System/Updates/UpdateChanges.tsx +++ /dev/null @@ -1,43 +0,0 @@ -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 new file mode 100644 index 000000000..c17ec1e6c --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,251 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +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', ['Prowlarr'])} +
+ + { + 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 deleted file mode 100644 index ea309a1cc..000000000 --- a/frontend/src/System/Updates/Updates.tsx +++ /dev/null @@ -1,303 +0,0 @@ -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 new file mode 100644 index 000000000..38873a990 --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,101 @@ +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/getIndexOfFirstCharacter.js b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js index a0dbc4d0d..5cbb30085 100644 --- a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js +++ b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js @@ -1,9 +1,9 @@ export default function getIndexOfFirstCharacter(items, character) { return items.findIndex((item) => { - const firstCharacter = 'sortName' in item ? item.sortName.charAt(0) : item.sortTitle.charAt(0); + const firstCharacter = item.sortTitle.charAt(0); if (character === '#') { - return !isNaN(Number(firstCharacter)); + return !isNaN(firstCharacter); } return firstCharacter === character; diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js new file mode 100644 index 000000000..1956d3bac --- /dev/null +++ b/frontend/src/Utilities/Array/sortByName.js @@ -0,0 +1,5 @@ +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 deleted file mode 100644 index 8fbde08c9..000000000 --- a/frontend/src/Utilities/Array/sortByProp.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/abbreviateNumber.js b/frontend/src/Utilities/Number/abbreviateNumber.js deleted file mode 100644 index f6c86a9d7..000000000 --- a/frontend/src/Utilities/Number/abbreviateNumber.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function abbreviateNumber(num, decimalPlaces) { - if (num === null) { - return null; - } - - if (num === 0) { - return '0'; - } - - decimalPlaces = (!decimalPlaces || decimalPlaces < 0) ? 0 : decimalPlaces; - - const b = (num).toPrecision(2).split('e'); - const k = b.length === 1 ? 0 : Math.floor(Math.min(b[1].slice(1), 14) / 3); - const c = k < 1 ? num.toFixed(0 + decimalPlaces) : (num / Math.pow(10, k * 3) ).toFixed(1 + decimalPlaces); - const d = c < 0 ? c : Math.abs(c); - const e = d + ['', 'K', 'M', 'B', 'T'][k]; - - return e; -} diff --git a/frontend/src/Utilities/Number/formatBytes.ts b/frontend/src/Utilities/Number/formatBytes.js similarity index 61% rename from frontend/src/Utilities/Number/formatBytes.ts rename to frontend/src/Utilities/Number/formatBytes.js index a0ae8a985..2fb3eebe6 100644 --- a/frontend/src/Utilities/Number/formatBytes.ts +++ b/frontend/src/Utilities/Number/formatBytes.js @@ -1,16 +1,16 @@ import { filesize } from 'filesize'; -function formatBytes(input: string | number) { +function formatBytes(input) { 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.js b/frontend/src/Utilities/String/translate.js new file mode 100644 index 000000000..2858014d0 --- /dev/null +++ b/frontend/src/Utilities/String/translate.js @@ -0,0 +1,30 @@ +import createAjaxRequest from 'Utilities/createAjaxRequest'; + +function getTranslations() { + let localization = null; + const ajaxOptions = { + async: false, + dataType: 'json', + url: '/localization', + success: function(data) { + localization = data.Strings; + } + }; + + createAjaxRequest(ajaxOptions); + + return localization; +} + +const translations = getTranslations(); + +export default function translate(key, args = '') { + if (args) { + const translatedKey = translate(key); + return translatedKey.replace(/\{(\d+)\}/g, (match, index) => { + return args[index]; + }); + } + + return translations[key] || key; +} diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts deleted file mode 100644 index 72d3adf40..000000000 --- a/frontend/src/Utilities/String/translate.ts +++ /dev/null @@ -1,42 +0,0 @@ -import createAjaxRequest from 'Utilities/createAjaxRequest'; - -function getTranslations() { - return createAjaxRequest({ - global: false, - dataType: 'json', - url: '/localization', - }).request; -} - -let translations: Record = {}; - -export async function fetchTranslations(): Promise { - return new Promise(async (resolve) => { - try { - const data = await getTranslations(); - translations = data.Strings; - - resolve(true); - } catch { - resolve(false); - } - }); -} - -export default function translate( - key: string, - tokens: Record = {} -) { - const translation = translations[key] || key; - - tokens.appName = 'Prowlarr'; - - // Fallback to the old behaviour for translations not yet updated to use named tokens - Object.values(tokens).forEach((value, index) => { - tokens[index] = value; - }); - - return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => - String(tokens[tokenMatch] ?? match) - ); -} diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js new file mode 100644 index 000000000..705f13a5d --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function getSelectedIds(selectedState, { parseIds = true } = {}) { + return _.reduce(selectedState, (result, value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, []); +} + +export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts deleted file mode 100644 index b84db6245..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { reduce } from 'lodash'; -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -function getSelectedIds(selectedState: SelectedState): number[] { - return reduce( - selectedState, - (result: number[], value, id) => { - if (value) { - result.push(parseInt(id)); - } - - return result; - }, - [] - ); -} - -export default getSelectedIds; diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js new file mode 100644 index 000000000..b687f2682 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.js @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path) { + return `${window.Prowlarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getPathWithUrlBase.ts b/frontend/src/Utilities/getPathWithUrlBase.ts deleted file mode 100644 index 948456728..000000000 --- a/frontend/src/Utilities/getPathWithUrlBase.ts +++ /dev/null @@ -1,3 +0,0 @@ -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 1b380851d..dae5150b7 100644 --- a/frontend/src/Utilities/getUniqueElementId.js +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -1,9 +1,7 @@ let i = 0; -/** - * @deprecated Use React's useId() instead - * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - */ +// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx deleted file mode 100644 index 5e9985ba3..000000000 --- a/frontend/src/bootstrap.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createBrowserHistory } from 'history'; -import React from 'react'; -import { render } from 'react-dom'; -import createAppStore from 'Store/createAppStore'; -import App from './App/App'; - -export async function bootstrap() { - const history = createBrowserHistory(); - const store = createAppStore(history); - - render( - , - document.getElementById('root') - ); -} diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 5efc89448..a2f8bcabc 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,16 +3,13 @@ + + - - - - - - + @@ -51,15 +48,7 @@ /> - - - - <% for (key in htmlWebpackPlugin.files.js) { %><% } %> - <% for (key in htmlWebpackPlugin.files.css) { %><% } %> + Prowlarr @@ -88,4 +77,7 @@
+ + + diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 000000000..59911154e --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,21 @@ +import { createBrowserHistory } from 'history'; +import React from 'react'; +import { render } from 'react-dom'; +import createAppStore from 'Store/createAppStore'; +import App from './App/App'; + +import './preload'; +import './polyfills'; +import 'Styles/globals.css'; +import './index.css'; + +const history = createBrowserHistory(); +const store = createAppStore(history); + +render( + , + document.getElementById('root') +); diff --git a/frontend/src/index.ts b/frontend/src/index.ts deleted file mode 100644 index 5c58019a2..000000000 --- a/frontend/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import './polyfills'; -import 'Styles/globals.css'; -import './index.css'; - -const initializeUrl = `${ - window.Prowlarr.urlBase -}/initialize.json?t=${Date.now()}`; -const response = await fetch(initializeUrl); - -window.Prowlarr = await response.json(); - -/* eslint-disable no-undef, @typescript-eslint/ban-ts-comment */ -// @ts-ignore 2304 -__webpack_public_path__ = `${window.Prowlarr.urlBase}/`; -/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ - -const { bootstrap } = await import('./bootstrap'); - -await bootstrap(); diff --git a/frontend/src/login.html b/frontend/src/login.html index d8af7f73f..dcfb23140 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,18 +3,15 @@ + + - - - - - - + body { - background-color: var(--pageBackground); - color: var(--textColor); + background-color: #f5f7fa; + color: #656565; font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } @@ -88,14 +85,14 @@ padding: 10px; border-top-left-radius: 4px; border-top-right-radius: 4px; - background-color: var(--themeDarkColor); + background-color: #464b51; } .panel-body { padding: 20px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; - background-color: var(--panelBackground); + background-color: #fff; } .sign-in { @@ -112,18 +109,16 @@ padding: 6px 16px; width: 100%; height: 35px; - background-color: var(--inputBackgroundColor); - border: 1px solid var(--inputBorderColor); + border: 1px solid #dde6e9; border-radius: 4px; - box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); - color: var(--textColor); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } .form-input:focus { outline: 0; - border-color: var(--inputFocusBorderColor); - box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), - 0 0 8px var(--inputFocusBoxShadowColor); + border-color: #66afe9; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 8px rgba(102, 175, 233, 0.6); } .button { @@ -132,10 +127,10 @@ padding: 10px 0; width: 100%; border: 1px solid; - border-color: var(--primaryBorderColor); + border-color: #5899eb; border-radius: 4px; - background-color: var(--primaryBackgroundColor); - color: var(--white); + background-color: #5d9cec; + color: #fff; vertical-align: middle; text-align: center; white-space: nowrap; @@ -143,9 +138,9 @@ } .button:hover { - border-color: var(--primaryHoverBorderColor); - background-color: var(--primaryHoverBackgroundColor); - color: var(--white); + border-color: #3483e7; + background-color: #4b91ea; + color: #fff; text-decoration: none; } @@ -167,24 +162,24 @@ .forgot-password { margin-left: auto; - color: var(--forgotPasswordColor); + color: #909fa7; text-decoration: none; font-size: 13px; } .forgot-password:focus, .forgot-password:hover { - color: var(--forgotPasswordAltColor); + color: #748690; text-decoration: underline; } .forgot-password:visited { - color: var(--forgotPasswordAltColor); + color: #748690; } .login-failed { margin-top: 20px; - color: var(--failedColor); + color: #f05050; font-size: 14px; } @@ -293,59 +288,5 @@ 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/Application.ts b/frontend/src/typings/Application.ts deleted file mode 100644 index 650429475..000000000 --- a/frontend/src/typings/Application.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export enum ApplicationSyncLevel { - Disabled = 'disabled', - AddOnly = 'addOnly', - FullSync = 'fullSync', -} - -interface Application extends ModelBase { - name: string; - syncLevel: ApplicationSyncLevel; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; - tags: number[]; -} - -export default Application; diff --git a/frontend/src/typings/DownloadClient.ts b/frontend/src/typings/DownloadClient.ts deleted file mode 100644 index 45af9eb32..000000000 --- a/frontend/src/typings/DownloadClient.ts +++ /dev/null @@ -1,26 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} - -interface DownloadClient extends ModelBase { - enable: boolean; - protocol: string; - priority: number; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; - tags: number[]; -} - -export default DownloadClient; diff --git a/frontend/src/typings/Health.ts b/frontend/src/typings/Health.ts deleted file mode 100644 index 66f385bbb..000000000 --- a/frontend/src/typings/Health.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 0e20206ef..000000000 --- a/frontend/src/typings/Helpers/KeysMatching.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 3e50355dc..000000000 --- a/frontend/src/typings/History.ts +++ /dev/null @@ -1,28 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export type HistoryQueryType = - | 'search' - | 'tvsearch' - | 'movie' - | 'book' - | 'music'; - -export interface HistoryData { - source: string; - host: string; - limit: number; - offset: number; - elapsedTime: number; - query: string; - queryType: HistoryQueryType; -} - -interface History extends ModelBase { - indexerId: number; - date: string; - successful: boolean; - eventType: string; - data: HistoryData; -} - -export default History; diff --git a/frontend/src/typings/IndexerStats.ts b/frontend/src/typings/IndexerStats.ts deleted file mode 100644 index ddbcebaec..000000000 --- a/frontend/src/typings/IndexerStats.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface IndexerStatsIndexer { - indexerId: number; - indexerName: string; - averageResponseTime: number; - averageGrabResponseTime: number; - numberOfQueries: number; - numberOfGrabs: number; - numberOfRssQueries: number; - numberOfAuthQueries: number; - numberOfFailedQueries: number; - numberOfFailedGrabs: number; - numberOfFailedRssQueries: number; - numberOfFailedAuthQueries: number; -} - -export interface IndexerStatsUserAgent { - userAgent: string; - numberOfQueries: number; - numberOfGrabs: number; -} - -export interface IndexerStatsHost { - host: string; - numberOfQueries: number; - numberOfGrabs: number; -} - -export interface IndexerStats { - indexers: IndexerStatsIndexer[]; - userAgents: IndexerStatsUserAgent[]; - hosts: IndexerStatsHost[]; -} diff --git a/frontend/src/typings/Notification.ts b/frontend/src/typings/Notification.ts deleted file mode 100644 index 63ea906c4..000000000 --- a/frontend/src/typings/Notification.ts +++ /dev/null @@ -1,33 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export interface Field { - order: number; - name: string; - label: string; - value: boolean | number | string; - type: string; - advanced: boolean; - privacy: string; -} - -interface Notification extends ModelBase { - enable: boolean; - name: string; - onGrab: boolean; - onHealthIssue: boolean; - onHealthRestored: boolean; - includeHealthWarnings: boolean; - onApplicationUpdate: boolean; - supportsOnGrab: boolean; - supportsOnHealthIssue: boolean; - supportsOnHealthRestored: boolean; - supportsOnApplicationUpdate: boolean; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; - tags: number[]; -} - -export default Notification; diff --git a/frontend/src/typings/Release.ts b/frontend/src/typings/Release.ts deleted file mode 100644 index 5b832b1da..000000000 --- a/frontend/src/typings/Release.ts +++ /dev/null @@ -1,28 +0,0 @@ -import ModelBase from 'App/ModelBase'; -import { IndexerCategory } from 'Indexer/Indexer'; - -interface Release extends ModelBase { - guid: string; - categories: IndexerCategory[]; - protocol: string; - title: string; - sortTitle: string; - fileName: string; - infoUrl: string; - downloadUrl?: string; - magnetUrl?: string; - indexerId: number; - indexer: string; - age: number; - ageHours: number; - ageMinutes: number; - publishDate: string; - size?: number; - files?: number; - grabs?: number; - seeders?: number; - leechers?: number; - indexerFlags: string[]; -} - -export default Release; diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts deleted file mode 100644 index c867bed74..000000000 --- a/frontend/src/typings/Settings/General.ts +++ /dev/null @@ -1,45 +0,0 @@ -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/Settings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts deleted file mode 100644 index 656c4518b..000000000 --- a/frontend/src/typings/Settings/UiSettings.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface UiSettings { - theme: 'auto' | 'dark' | 'light'; - showRelativeDates: boolean; - shortDateFormat: string; - longDateFormat: string; - timeFormat: string; -} diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts deleted file mode 100644 index d5eab3ca3..000000000 --- a/frontend/src/typings/SystemStatus.ts +++ /dev/null @@ -1,36 +0,0 @@ -interface SystemStatus { - appData: string; - appName: string; - authentication: string; - branch: string; - buildTime: string; - databaseVersion: string; - databaseType: string; - instanceName: string; - isAdmin: boolean; - isDebug: boolean; - isDocker: boolean; - isLinux: boolean; - isNetCore: boolean; - isOsx: boolean; - isProduction: boolean; - isUserInteractive: boolean; - isWindows: boolean; - migrationVersion: number; - mode: string; - osName: string; - osVersion: string; - packageAuthor: string; - packageUpdateMechanism: string; - packageUpdateMechanismMessage: string; - packageVersion: string; - runtimeName: string; - runtimeVersion: string; - sqliteVersion: string; - startTime: string; - startupPath: string; - urlBase: string; - version: string; -} - -export default SystemStatus; diff --git a/frontend/src/typings/Table.ts b/frontend/src/typings/Table.ts deleted file mode 100644 index 4f99e2045..000000000 --- a/frontend/src/typings/Table.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 57895d73e..000000000 --- a/frontend/src/typings/Task.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 448b1728d..000000000 --- a/frontend/src/typings/Update.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/frontend/src/typings/callbacks.ts b/frontend/src/typings/callbacks.ts deleted file mode 100644 index 0114efeb0..000000000 --- a/frontend/src/typings/callbacks.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SortDirection from 'Helpers/Props/SortDirection'; - -export type SortCallback = ( - sortKey: string, - sortDirection: SortDirection -) => void; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts deleted file mode 100644 index c0fda305c..000000000 --- a/frontend/src/typings/inputs.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CheckInputChanged = { - name: string; - value: boolean; -}; diff --git a/frontend/src/typings/props.ts b/frontend/src/typings/props.ts deleted file mode 100644 index 5b87e36b3..000000000 --- a/frontend/src/typings/props.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface SelectStateInputProps { - id: number; - value: boolean; - shiftKey: boolean; -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 611c872ed..dfddb15a3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,21 +1,13 @@ { "compilerOptions": { - "target": "esnext", + "target": "es6", "allowJs": true, "checkJs": false, "baseUrl": "src", "jsx": "react", - "module": "esnext", + "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, "noEmit": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "strict": true, "esModuleInterop": true, "typeRoots": ["node_modules/@types", "typings"], "paths": { diff --git a/package.json b/package.json index 25960d641..62a38d710 100644 --- a/package.json +++ b/package.json @@ -5,52 +5,57 @@ "scripts": { "build": "webpack --config ./frontend/build/webpack.config.js", "prebuild": "yarn clean", - "clean": "rimraf ./_output/UI && rimraf --glob \"**/*.js.map\"", + "clean": "rimraf ./_output/UI && rimraf -g \"**/*.js.map\"", "start": "webpack --watch --config ./frontend/build/webpack.config.js", "watch": "webpack --watch --config ./frontend/build/webpack.config.js", "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", "author": "Team Prowlarr", "license": "GPL-3.0", "readmeFilename": "readme.md", - "main": "index.ts", + "main": "index.js", "browserslist": [ - "defaults" + ">0.25%", + "not ie 11", + "not op_mini all", + "not chrome < 60" ], "dependencies": { - "@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", + "@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", "@juggle/resize-observer": "3.4.0", - "@microsoft/signalr": "6.0.25", - "@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", + "@microsoft/signalr": "6.0.16", + "@sentry/browser": "7.46.0", + "@sentry/integrations": "7.46.0", + "@types/jest": "29.5.0", + "@types/node": "18.15.11", + "@types/react": "18.0.31", + "@types/react-dom": "18.0.11", + "chart.js": "4.2.1", + "classnames": "2.3.2", + "clipboard": "2.0.11", "connected-react-router": "6.9.3", - "copy-to-clipboard": "3.3.3", "element-class": "0.2.2", - "filesize": "10.1.6", + "filesize": "10.0.7", "history": "4.10.1", + "https-browserify": "1.0.0", "jdu": "1.0.0", - "jquery": "3.7.1", + "jquery": "3.6.4", "lodash": "4.17.21", "mobile-detect": "1.4.5", - "moment": "2.30.1", + "moment": "2.29.4", "mousetrap": "1.6.5", "normalize.css": "8.0.1", "prop-types": "15.8.1", - "qs": "6.13.0", + "qs": "6.11.1", "react": "17.0.2", "react-addons-shallow-compare": "15.6.3", "react-async-script": "1.2.0", @@ -64,82 +69,86 @@ "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", "react-router": "5.2.0", "react-router-dom": "5.2.0", - "react-tabs": "4.3.0", "react-text-truncate": "0.19.0", "react-use-measure": "2.1.1", - "react-virtualized": "9.21.1", - "react-window": "1.8.10", + "react-virtualized": "9.22.3", + "react-window": "1.8.8", "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.8", + "reselect": "4.1.7", "stacktrace-js": "2.0.2", - "typescript": "5.7.2" + "typescript": "5.0.3" }, "devDependencies": { - "@babel/core": "7.26.0", - "@babel/eslint-parser": "7.25.9", - "@babel/plugin-proposal-export-default-from": "7.25.9", + "@babel/core": "7.21.3", + "@babel/eslint-parser": "7.21.3", + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-decorators": "7.21.0", + "@babel/plugin-proposal-export-default-from": "7.18.10", + "@babel/plugin-proposal-export-namespace-from": "7.18.9", + "@babel/plugin-proposal-function-sent": "7.18.6", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-proposal-numeric-separator": "7.18.6", + "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/plugin-proposal-throw-expressions": "7.18.6", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@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.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", + "@babel/preset-env": "7.20.2", + "@babel/preset-react": "7.18.6", + "@babel/preset-typescript": "7.21.0", + "@types/react-window": "1.8.5", + "@typescript-eslint/eslint-plugin": "5.57.0", + "@typescript-eslint/parser": "5.57.0", "are-you-es5": "2.1.2", - "autoprefixer": "10.4.20", - "babel-loader": "9.2.1", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.39.0", + "core-js": "3.29.1", "css-loader": "6.7.3", "css-modules-typescript-loader": "4.0.1", - "eslint": "8.57.1", - "eslint-config-prettier": "8.10.0", + "eslint": "8.37.0", + "eslint-config-prettier": "8.8.0", "eslint-plugin-filenames": "1.3.2", - "eslint-plugin-import": "2.31.0", + "eslint-plugin-import": "2.27.5", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.37.1", - "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-simple-import-sort": "12.1.1", + "eslint-plugin-react": "7.32.2", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-simple-import-sort": "10.0.0", "file-loader": "6.2.0", "filemanager-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0", - "html-webpack-plugin": "5.6.0", + "html-webpack-plugin": "5.5.0", "loader-utils": "^3.2.1", - "mini-css-extract-plugin": "2.9.1", - "postcss": "8.4.47", + "mini-css-extract-plugin": "2.7.5", + "postcss": "8.4.21", "postcss-color-function": "4.1.0", - "postcss-loader": "7.3.0", + "postcss-loader": "7.1.0", "postcss-mixins": "9.0.4", - "postcss-nested": "6.2.0", + "postcss-nested": "6.0.1", "postcss-simple-vars": "7.0.1", "postcss-url": "10.1.3", - "prettier": "2.8.8", + "prettier": "2.8.7", "require-nocache": "1.0.0", - "rimraf": "6.0.1", + "rimraf": "4.4.1", + "run-sequence": "2.2.1", + "streamqueue": "1.1.2", "style-loader": "3.3.2", - "stylelint": "15.6.1", - "stylelint-order": "6.0.4", - "terser-webpack-plugin": "5.3.10", - "ts-loader": "9.5.1", - "typescript-plugin-css-modules": "5.0.1", + "stylelint": "14.16.0", + "stylelint-order": "5.0.0", + "ts-loader": "9.4.2", + "typescript-plugin-css-modules": "5.0.0", "url-loader": "4.1.1", - "webpack": "5.95.0", - "webpack-cli": "5.1.4", + "webpack": "5.77.0", + "webpack-cli": "5.0.1", "webpack-livereload-plugin": "3.0.2" } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ce3672c38..f9b5c9b64 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,9 +3,7 @@ 6.0-all true - true - false - + false AnyCPU true win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-musl-arm;linux-arm;linux-arm64;linux-musl-arm64 @@ -26,17 +24,10 @@ true true true - + false true - - - true - - $(NoWarn);CS1591 @@ -59,7 +50,7 @@ true - + true @@ -72,7 +63,7 @@ Prowlarr prowlarr.com Copyright 2014-$([System.DateTime]::Now.ToString('yyyy')) prowlarr.com (GNU General Public v3) - + 10.0.0.* $(Configuration)-dev @@ -81,7 +72,7 @@ false false false - + False $(MSBuildProjectDirectory)=./$(MSBuildProjectName)/ @@ -99,51 +90,19 @@ $(MSBuildProjectName.Replace('Prowlarr','NzbDrone')) - - - - - - - - - - - - - - - - - - true - - - - true - - true - - - - - - - - - - - - - - false + + + + + + + + + @@ -176,52 +135,22 @@ - - - - - x64 - - - - - x86 - - - - - arm64 - - - - - arm - - - - - - - - - <_UsingDefaultRuntimeIdentifier>true - win-$(Architecture) + win-x64 <_UsingDefaultRuntimeIdentifier>true - linux-$(Architecture) + linux-x64 <_UsingDefaultRuntimeIdentifier>true - osx-$(Architecture) + osx-x64 diff --git a/src/NuGet.config b/src/NuGet.config index fcbd8bafb..19fea7384 100644 --- a/src/NuGet.config +++ b/src/NuGet.config @@ -7,6 +7,5 @@ - diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index ae1bcc5e1..247f52633 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -11,6 +11,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Test.Common; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Remote; namespace NzbDrone.Automation.Test { @@ -67,7 +68,7 @@ namespace NzbDrone.Automation.Test { try { - var image = ((ITakesScreenshot)driver).GetScreenshot(); + Screenshot image = ((ITakesScreenshot)driver).GetScreenshot(); image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png); } catch (Exception ex) diff --git a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs index 02078c47e..4c9a17581 100644 --- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs +++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs @@ -36,13 +36,9 @@ namespace NzbDrone.Automation.Test.PageModel { try { - var element = d.FindElement(By.ClassName("followingBalls")); + IWebElement element = d.FindElement(By.ClassName("followingBalls")); return !element.Displayed; } - catch (StaleElementReferenceException) - { - return true; - } catch (NoSuchElementException) { return true; diff --git a/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj b/src/NzbDrone.Automation.Test/Prowlarr.Automation.Test.csproj index 78c8b7d0f..bb0b5fcc4 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/CacheTests/CachedFixture.cs b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs index bdbd45aca..7c892047d 100644 --- a/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs +++ b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using FluentAssertions; using NUnit.Framework; @@ -65,9 +65,9 @@ namespace NzbDrone.Common.Test.CacheTests [Test] public void should_store_null() { - var hitCount = 0; + int hitCount = 0; - for (var i = 0; i < 10; i++) + for (int i = 0; i < 10; i++) { _cachedString.Get("key", () => { @@ -83,10 +83,10 @@ namespace NzbDrone.Common.Test.CacheTests [Platform(Exclude = "MacOsX")] public void should_honor_ttl() { - var hitCount = 0; + int hitCount = 0; _cachedString = new Cached(); - for (var i = 0; i < 10; i++) + for (int i = 0; i < 10; i++) { _cachedString.Get("key", () => diff --git a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index e9d4aa3b0..91564beb3 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; +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; @@ -45,26 +43,6 @@ 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] @@ -164,7 +142,7 @@ namespace NzbDrone.Common.Test [Test] public void SaveDictionary_should_save_proper_value() { - var port = 20555; + int port = 20555; var dic = Subject.GetConfigDictionary(); dic["Port"] = 20555; @@ -177,9 +155,9 @@ namespace NzbDrone.Common.Test [Test] public void SaveDictionary_should_only_save_specified_values() { - var port = 20555; - var origSslPort = 20551; - var sslPort = 20552; + int port = 20555; + int origSslPort = 20551; + int sslPort = 20552; var dic = Subject.GetConfigDictionary(); dic["Port"] = port; diff --git a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs index dd27f6f1b..78aa99f7d 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs @@ -42,33 +42,33 @@ namespace NzbDrone.Common.Test.DiskTests [Test] public void should_not_contain_recycling_bin_for_root_of_drive() { - var root = @"C:\".AsOsAgnostic(); + string root = @"C:\".AsOsAgnostic(); SetupFolders(root); Mocker.GetMock() .Setup(s => s.GetDirectoryInfos(It.IsAny())) .Returns(_folders); - Subject.LookupContents(root, false, false).Directories.Should().NotContain(dir => dir.Path == Path.Combine(root, RECYCLING_BIN)); + Subject.LookupContents(root, false, false).Directories.Should().NotContain(Path.Combine(root, RECYCLING_BIN)); } [Test] public void should_not_contain_system_volume_information() { - var root = @"C:\".AsOsAgnostic(); + string root = @"C:\".AsOsAgnostic(); SetupFolders(root); Mocker.GetMock() .Setup(s => s.GetDirectoryInfos(It.IsAny())) .Returns(_folders); - Subject.LookupContents(root, false, false).Directories.Should().NotContain(dir => dir.Path == Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); + Subject.LookupContents(root, false, false).Directories.Should().NotContain(Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); } [Test] public void should_not_contain_recycling_bin_or_system_volume_information_for_root_of_drive() { - var root = @"C:\".AsOsAgnostic(); + string root = @"C:\".AsOsAgnostic(); SetupFolders(root); Mocker.GetMock() diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index fac3e20e7..9e6799d43 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -351,26 +352,6 @@ namespace NzbDrone.Common.Test.DiskTests .Verify(v => v.DeleteFile(_targetPath), Times.Once()); } - [Test] - public void should_not_rollback_move_on_partial_if_destination_already_exists() - { - Mocker.GetMock() - .Setup(v => v.MoveFile(_sourcePath, _targetPath, false)) - .Callback(() => - { - WithExistingFile(_targetPath, true, 900); - }); - - Mocker.GetMock() - .Setup(v => v.MoveFile(_sourcePath, _targetPath, false)) - .Throws(new FileAlreadyExistsException("File already exists", _targetPath)); - - Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move)); - - Mocker.GetMock() - .Verify(v => v.DeleteFile(_targetPath), Times.Never()); - } - [Test] public void should_log_error_if_rollback_partialmove_fails() { @@ -564,7 +545,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName, destination.FullName); - count.Should().Be(0); + count.Should().Equals(0); destination.GetFileSystemInfos().Should().BeEmpty(); } @@ -584,7 +565,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName, destination.FullName); - count.Should().Be(0); + count.Should().Equals(0); destination.GetFileSystemInfos().Should().HaveCount(1); } @@ -601,7 +582,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName, destination.FullName); - count.Should().Be(3); + count.Should().Equals(3); VerifyCopyFolder(original.FullName, destination.FullName); } @@ -618,7 +599,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName, destination.FullName); - count.Should().Be(3); + count.Should().Equals(3); File.Exists(Path.Combine(destination.FullName, _nfsFile)).Should().BeFalse(); } @@ -638,7 +619,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName, destination.FullName); - count.Should().Be(0); + count.Should().Equals(0); VerifyCopyFolder(original.FullName, destination.FullName); } @@ -655,7 +636,7 @@ namespace NzbDrone.Common.Test.DiskTests var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName); - count.Should().Be(3); + count.Should().Equals(3); VerifyCopyFolder(original.FullName, destination.FullName); } @@ -837,7 +818,7 @@ namespace NzbDrone.Common.Test.DiskTests // Note: never returns anything. Mocker.GetMock() - .Setup(v => v.GetFileInfos(It.IsAny(), false)) + .Setup(v => v.GetFileInfos(It.IsAny(), SearchOption.TopDirectoryOnly)) .Returns(new List()); Mocker.GetMock() @@ -875,8 +856,8 @@ namespace NzbDrone.Common.Test.DiskTests .Returns(v => new DirectoryInfo(v).GetDirectories().ToList()); Mocker.GetMock() - .Setup(v => v.GetFileInfos(It.IsAny(), false)) - .Returns((v, _) => new DirectoryInfo(v).GetFiles().ToList()); + .Setup(v => v.GetFileInfos(It.IsAny(), SearchOption.TopDirectoryOnly)) + .Returns((v, _) => new DirectoryInfo(v).GetFiles().ToList()); Mocker.GetMock() .Setup(v => v.GetFileSize(It.IsAny())) diff --git a/src/NzbDrone.Common.Test/EnsureTest/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/EnsureTest/PathExtensionFixture.cs index 42a65f9b1..53c1b42f4 100644 --- a/src/NzbDrone.Common.Test/EnsureTest/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/EnsureTest/PathExtensionFixture.cs @@ -1,5 +1,4 @@ using NUnit.Framework; -using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Test.Common; @@ -13,14 +12,14 @@ namespace NzbDrone.Common.Test.EnsureTest public void EnsureWindowsPath(string path) { WindowsOnly(); - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); } [TestCase(@"/var/user/file with, comma.mkv")] public void EnsureLinuxPath(string path) { PosixOnly(); - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); } } } diff --git a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs index 0f7ad3004..ff5d7383e 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/IPAddressExtensionsFixture.cs @@ -21,28 +21,9 @@ 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/NumberExtensionFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs similarity index 91% rename from src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs rename to src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs index c51ab7ad4..76e28f3f7 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/Int64ExtensionFixture.cs @@ -1,11 +1,11 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Test.ExtensionTests { [TestFixture] - public class NumberExtensionFixture + public class Int64ExtensionFixture { [TestCase(0, "0 B")] [TestCase(1000, "1,000.0 B")] diff --git a/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/IsValidIPAddressFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/IsValidIPAddressFixture.cs index 8a049f068..0e2ac3d63 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/IsValidIPAddressFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/IsValidIPAddressFixture.cs @@ -1,3 +1,4 @@ +using System.Globalization; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 43620edf4..9b48e77ca 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -128,16 +128,6 @@ namespace NzbDrone.Common.Test.Http response.Content.Should().NotBeNullOrWhiteSpace(); } - [Test] - public void should_throw_timeout_request() - { - var request = new HttpRequest($"https://{_httpBinHost}/delay/10"); - - request.RequestTimeout = new TimeSpan(0, 0, 5); - - Assert.ThrowsAsync(async () => await Subject.ExecuteAsync(request)); - } - [Test] public void should_execute_https_get() { @@ -798,7 +788,7 @@ namespace NzbDrone.Common.Test.Http try { // the date is bad in the below - should be 13-Jul-2026 - var malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly"; + string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly"; var requestSet = new HttpRequestBuilder($"https://{_httpBinHost}/response-headers") .AddQueryParam("Set-Cookie", malformedCookie) .Build(); @@ -832,7 +822,7 @@ namespace NzbDrone.Common.Test.Http { try { - var url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}"; + string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}"; var requestSet = new HttpRequest(url); requestSet.AllowAutoRedirect = false; diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 9e2b31d87..8f2b4a4eb 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -10,9 +10,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests // Indexer Urls [TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")] [TestCase(@"http://rss.torrentleech.org/mySecret")] - [TestCase(@"https://rss24h.torrentleech.org/mySecret")] - [TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")] - [TestCase(@"https://www.torrentleech.org/rss/download/12345/01233210/file.name-RLSGRP.torrent")] + [TestCase(@"http://rss.torrentleech.org/rss/download/12345/01233210/filename.torrent")] [TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")] [TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")] [TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")] @@ -29,8 +27,6 @@ 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 @@ -45,8 +41,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests // danish bytes response [TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")] [TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")] - [TestCase(@"{""rsskey"":""2b51db35e1910123321025a12b9933d2""}")] - [TestCase(@"{""passkey"":""2b51db35e1910123321025a12b9933d2""}")] // nzbgeek & usenet response [TestCase(@"https://api.nzbgeek.info/api?t=details&id=2b51db35e1910123321025a12b9933d2&apikey=2b51db35e1910123321025a12b9933d2")] @@ -85,22 +79,13 @@ namespace NzbDrone.Common.Test.InstrumentationTests // Deluge [TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")] [TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")] - [TestCase(@",{""download_location"": ""/Users/mySecret/Downloads""}")] [TestCase(@"auth.login(""mySecret"")")] // Download Station [TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")] - // Announce URLs (passkeys) Magnet & Tracker - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")] - [TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")] - [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")] - [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")] - [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")] - [TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")] + // Tracker Responses + [TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")] // BroadcastheNet [TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")] @@ -114,17 +99,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests // RSS [TestCase(@"")] - // Applications - [TestCase(@"""name"":""apiKey"",""value"":""mySecret""")] - // Internal [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 b60fe2e54..7392c3b85 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs @@ -4,7 +4,6 @@ using System.Linq; using FluentAssertions; using NLog; using NUnit.Framework; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Test.Common; @@ -27,7 +26,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests [SetUp] public void Setup() { - _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock().Object); + _subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111"); } 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 010bc3a02..a01c6dc1a 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -3,7 +3,6 @@ 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; @@ -35,8 +34,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(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")] + [TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")] public void Clean_Path_Windows(string dirty, string clean) { WindowsOnly(); @@ -133,16 +131,11 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\test\", @"C:\Test\mydir")] [TestCase(@"C:\test", @"C:\Test\mydir\")] - public void windows_path_should_be_parent(string parentPath, string childPath) + public void path_should_be_parent_on_windows_only(string parentPath, string childPath) { - parentPath.IsParentPath(childPath).Should().Be(true); - } + var expectedResult = OsInfo.IsWindows; - [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); + parentPath.IsParentPath(childPath).Should().Be(expectedResult); } [TestCase(@"C:\Test\mydir", @"C:\Test")] @@ -150,57 +143,20 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] - public void windows_path_should_return_parent(string path, string parentPath) + public void path_should_return_parent_windows(string path, string parentPath) { + WindowsOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] - [TestCase(@"/test/tv", "/test")] - public void unix_path_should_return_parent(string path, string parentPath) + public void path_should_return_parent_mono(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() { @@ -208,7 +164,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); @@ -358,80 +314,5 @@ 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 237febe74..9c348316f 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -10,7 +10,6 @@ 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; @@ -30,16 +29,10 @@ 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 d420bbbc0..2dee4d822 100644 --- a/src/NzbDrone.Common/ArchiveService.cs +++ b/src/NzbDrone.Common/ArchiveService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.GZip; @@ -12,7 +11,7 @@ namespace NzbDrone.Common public interface IArchiveService { void Extract(string compressedFile, string destination); - void CreateZip(string path, IEnumerable files); + void CreateZip(string path, params string[] files); } public class ArchiveService : IArchiveService @@ -40,20 +39,19 @@ namespace NzbDrone.Common _logger.Debug("Extraction complete."); } - public void CreateZip(string path, IEnumerable files) + public void CreateZip(string path, params string[] files) { - _logger.Debug("Creating archive {0}", path); - - using var zipFile = ZipFile.Create(path); - - zipFile.BeginUpdate(); - - foreach (var file in files) + using (var zipFile = ZipFile.Create(path)) { - zipFile.Add(file, Path.GetFileName(file)); - } + zipFile.BeginUpdate(); - zipFile.CommitUpdate(); + foreach (var file in files) + { + zipFile.Add(file, Path.GetFileName(file)); + } + + zipFile.CommitUpdate(); + } } private void ExtractZip(string compressedFile, string destination) @@ -76,17 +74,17 @@ namespace NzbDrone.Common continue; // Ignore directories } - var entryFileName = zipEntry.Name; + string entryFileName = zipEntry.Name; // to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName); // Optionally match entrynames against a selection list here to skip as desired. // The unpacked length is available in the zipEntry.Size property. - var buffer = new byte[4096]; // 4K is optimum - var zipStream = zipFile.GetInputStream(zipEntry); + byte[] buffer = new byte[4096]; // 4K is optimum + Stream zipStream = zipFile.GetInputStream(zipEntry); // Manipulate the output filename here as desired. - var fullZipToPath = Path.Combine(destination, entryFileName); - var directoryName = Path.GetDirectoryName(fullZipToPath); + string fullZipToPath = Path.Combine(destination, entryFileName); + string directoryName = Path.GetDirectoryName(fullZipToPath); if (directoryName.Length > 0) { Directory.CreateDirectory(directoryName); @@ -95,7 +93,7 @@ namespace NzbDrone.Common // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size // of the file, but does not waste memory. // The "using" will close the stream even if an exception occurs. - using (var streamWriter = File.Create(fullZipToPath)) + using (FileStream streamWriter = File.Create(fullZipToPath)) { StreamUtils.Copy(zipStream, streamWriter, buffer); } @@ -108,7 +106,7 @@ namespace NzbDrone.Common Stream inStream = File.OpenRead(compressedFile); Stream gzipStream = new GZipInputStream(inStream); - var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null); + TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null); tarArchive.ExtractContents(destination); tarArchive.Close(); diff --git a/src/NzbDrone.Common/Cache/Cached.cs b/src/NzbDrone.Common/Cache/Cached.cs index 42463f682..a75116417 100644 --- a/src/NzbDrone.Common/Cache/Cached.cs +++ b/src/NzbDrone.Common/Cache/Cached.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Cache { @@ -47,7 +48,8 @@ namespace NzbDrone.Common.Cache public T Find(string key) { - if (!_store.TryGetValue(key, out var cacheItem)) + CacheItem cacheItem; + if (!_store.TryGetValue(key, out cacheItem)) { return default(T); } @@ -75,7 +77,8 @@ namespace NzbDrone.Common.Cache public void Remove(string key) { - _store.TryRemove(key, out _); + CacheItem value; + _store.TryRemove(key, out value); } public int Count => _store.Count; @@ -86,7 +89,9 @@ namespace NzbDrone.Common.Cache lifeTime = lifeTime ?? _defaultLifeTime; - if (_store.TryGetValue(key, out var cacheItem) && !cacheItem.IsExpired()) + CacheItem cacheItem; + + if (_store.TryGetValue(key, out cacheItem) && !cacheItem.IsExpired()) { if (_rollingExpiry && lifeTime.HasValue) { diff --git a/src/NzbDrone.Common/Cache/CachedDictionary.cs b/src/NzbDrone.Common/Cache/CachedDictionary.cs index 922b45835..6332f5054 100644 --- a/src/NzbDrone.Common/Cache/CachedDictionary.cs +++ b/src/NzbDrone.Common/Cache/CachedDictionary.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -86,7 +86,9 @@ namespace NzbDrone.Common.Cache { RefreshIfExpired(); - if (!_items.TryGetValue(key, out var result)) + TValue result; + + if (!_items.TryGetValue(key, out result)) { throw new KeyNotFoundException(string.Format("Item {0} not found in cache.", key)); } @@ -98,7 +100,9 @@ namespace NzbDrone.Common.Cache { RefreshIfExpired(); - _items.TryGetValue(key, out var result); + TValue result; + + _items.TryGetValue(key, out result); return result; } @@ -124,7 +128,8 @@ namespace NzbDrone.Common.Cache public void Remove(string key) { - _items.TryRemove(key, out _); + TValue item; + _items.TryRemove(key, out item); } } } diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 621d4b258..29de0c1a6 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Common.Disk { CheckFolderExists(path); - var dirFiles = GetFiles(path, true).ToList(); + var dirFiles = GetFiles(path, SearchOption.AllDirectories).ToList(); if (!dirFiles.Any()) { @@ -65,7 +65,7 @@ namespace NzbDrone.Common.Disk private void CheckFolderExists(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); if (!FolderExists(path)) { @@ -75,7 +75,7 @@ namespace NzbDrone.Common.Disk private void CheckFileExists(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); if (!FileExists(path)) { @@ -93,19 +93,19 @@ namespace NzbDrone.Common.Disk public bool FolderExists(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); return Directory.Exists(path); } public bool FileExists(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); return FileExists(path, PathStringComparison); } public bool FileExists(string path, StringComparison stringComparison) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); switch (stringComparison) { @@ -125,7 +125,7 @@ namespace NzbDrone.Common.Disk public bool FolderWritable(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); try { @@ -144,44 +144,35 @@ namespace NzbDrone.Common.Disk public bool FolderEmpty(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); return Directory.EnumerateFileSystemEntries(path).Empty(); } - public IEnumerable GetDirectories(string path) + public string[] GetDirectories(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); - return Directory.EnumerateDirectories(path, "*", new EnumerationOptions - { - AttributesToSkip = FileAttributes.System, - IgnoreInaccessible = true - }); + return Directory.GetDirectories(path); } - public IEnumerable GetFiles(string path, bool recursive) + public string[] GetFiles(string path, SearchOption searchOption) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); - return Directory.EnumerateFiles(path, "*", new EnumerationOptions - { - AttributesToSkip = FileAttributes.System, - RecurseSubdirectories = recursive, - IgnoreInaccessible = true - }); + return Directory.GetFiles(path, "*.*", searchOption); } public long GetFolderSize(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); - return GetFiles(path, true).Sum(e => new FileInfo(e).Length); + return GetFiles(path, SearchOption.AllDirectories).Sum(e => new FileInfo(e).Length); } public long GetFileSize(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); if (!FileExists(path)) { @@ -189,37 +180,18 @@ 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; } public void CreateFolder(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); Directory.CreateDirectory(path); } public void DeleteFile(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); Logger.Trace("Deleting file: {0}", path); RemoveReadOnly(path); @@ -229,8 +201,8 @@ namespace NzbDrone.Common.Disk public void CloneFile(string source, string destination, bool overwrite = false) { - Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(source, () => source).IsValidPath(); + Ensure.That(destination, () => destination).IsValidPath(); if (source.PathEquals(destination)) { @@ -247,8 +219,8 @@ namespace NzbDrone.Common.Disk public void CopyFile(string source, string destination, bool overwrite = false) { - Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(source, () => source).IsValidPath(); + Ensure.That(destination, () => destination).IsValidPath(); if (source.PathEquals(destination)) { @@ -265,8 +237,8 @@ namespace NzbDrone.Common.Disk public void MoveFile(string source, string destination, bool overwrite = false) { - Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(source, () => source).IsValidPath(); + Ensure.That(destination, () => destination).IsValidPath(); if (source.PathEquals(destination)) { @@ -284,19 +256,14 @@ namespace NzbDrone.Common.Disk public void MoveFolder(string source, string destination, bool overwrite = false) { - Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(source, () => source).IsValidPath(); + Ensure.That(destination, () => destination).IsValidPath(); Directory.Move(source, destination); } protected virtual void MoveFileInternal(string source, string destination) { - if (File.Exists(destination)) - { - throw new FileAlreadyExistsException("File already exists", destination); - } - File.Move(source, destination); } @@ -314,25 +281,24 @@ namespace NzbDrone.Common.Disk public void DeleteFolder(string path, bool recursive) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); - var files = GetFiles(path, recursive); - - files.ToList().ForEach(RemoveReadOnly); + var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + Array.ForEach(files, RemoveReadOnly); Directory.Delete(path, recursive); } public string ReadAllText(string filePath) { - Ensure.That(filePath, () => filePath).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(filePath, () => filePath).IsValidPath(); return File.ReadAllText(filePath); } public void WriteAllText(string filename, string contents) { - Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(filename, () => filename).IsValidPath(); RemoveReadOnly(filename); // File.WriteAllText is broken on net core when writing to some CIFS mounts @@ -348,7 +314,7 @@ namespace NzbDrone.Common.Disk public void FolderSetLastWriteTime(string path, DateTime dateTime) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); if (dateTime.Before(DateTimeExtensions.Epoch)) { @@ -360,7 +326,7 @@ namespace NzbDrone.Common.Disk public void FileSetLastWriteTime(string path, DateTime dateTime) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); if (dateTime.Before(DateTimeExtensions.Epoch)) { @@ -387,14 +353,14 @@ namespace NzbDrone.Common.Disk public virtual string GetPathRoot(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); return Path.GetPathRoot(path); } public string GetParentFolder(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); var parent = Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar)); @@ -441,9 +407,9 @@ namespace NzbDrone.Common.Disk public void EmptyFolder(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); - foreach (var file in GetFiles(path, false)) + foreach (var file in GetFiles(path, SearchOption.TopDirectoryOnly)) { DeleteFile(file); } @@ -530,7 +496,7 @@ namespace NzbDrone.Common.Disk public List GetDirectoryInfos(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); var di = new DirectoryInfo(path); @@ -539,23 +505,18 @@ namespace NzbDrone.Common.Disk public FileInfo GetFileInfo(string path) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); return new FileInfo(path); } - public List GetFileInfos(string path, bool recursive = false) + public List GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly) { - Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(path, () => path).IsValidPath(); var di = new DirectoryInfo(path); - return di.EnumerateFiles("*", new EnumerationOptions - { - AttributesToSkip = FileAttributes.System, - RecurseSubdirectories = recursive, - IgnoreInaccessible = true - }).ToList(); + return di.GetFiles("*", searchOption).ToList(); } public void RemoveEmptySubfolders(string path) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index fb7d93f48..44d28a9df 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -43,8 +43,8 @@ namespace NzbDrone.Common.Disk public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode) { - Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(sourcePath, () => sourcePath).IsValidPath(); + Ensure.That(targetPath, () => targetPath).IsValidPath(); sourcePath = ResolveRealParentPath(sourcePath); targetPath = ResolveRealParentPath(targetPath); @@ -140,8 +140,8 @@ namespace NzbDrone.Common.Disk { var filesCopied = 0; - Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(sourcePath, () => sourcePath).IsValidPath(); + Ensure.That(targetPath, () => targetPath).IsValidPath(); sourcePath = ResolveRealParentPath(sourcePath); targetPath = ResolveRealParentPath(targetPath); @@ -255,8 +255,8 @@ namespace NzbDrone.Common.Disk public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false) { - Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs); - Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs); + Ensure.That(sourcePath, () => sourcePath).IsValidPath(); + Ensure.That(targetPath, () => targetPath).IsValidPath(); sourcePath = ResolveRealParentPath(sourcePath); targetPath = ResolveRealParentPath(targetPath); @@ -500,13 +500,9 @@ namespace NzbDrone.Common.Disk throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize)); } } - catch (Exception ex) + catch { - if (ex is not FileAlreadyExistsException) - { - RollbackPartialMove(sourcePath, targetPath); - } - + RollbackPartialMove(sourcePath, targetPath); throw; } } diff --git a/src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs b/src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs deleted file mode 100644 index 69acb4cd7..000000000 --- a/src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace NzbDrone.Common.Disk -{ - public class FileAlreadyExistsException : Exception - { - public string Filename { get; set; } - - public FileAlreadyExistsException(string message, string filename) - : base(message) - { - Filename = filename; - } - } -} diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs index 8b9d1bae0..63529697e 100644 --- a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Common.Disk if ( allowFoldersWithoutTrailingSlashes && - query.IsPathValid(PathValidationType.CurrentOs) && + query.IsPathValid() && _diskProvider.FolderExists(query)) { return GetResult(query, includeFiles); diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 46589411a..4f1cad811 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; namespace NzbDrone.Common.Disk { @@ -22,8 +24,8 @@ namespace NzbDrone.Common.Disk bool FileExists(string path, StringComparison stringComparison); bool FolderWritable(string path); bool FolderEmpty(string path); - IEnumerable GetDirectories(string path); - IEnumerable GetFiles(string path, bool recursive); + string[] GetDirectories(string path); + string[] GetFiles(string path, SearchOption searchOption); long GetFolderSize(string path); long GetFileSize(string path); void CreateFolder(string path); @@ -52,7 +54,7 @@ namespace NzbDrone.Common.Disk IMount GetMount(string path); List GetDirectoryInfos(string path); FileInfo GetFileInfo(string path); - List GetFileInfos(string path, bool recursive = false); + List GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly); void RemoveEmptySubfolders(string path); void SaveStream(Stream stream, string path); bool IsValidFolderPermissionMask(string mask); diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index 30661d747..c4327ddeb 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Linq; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -10,8 +10,6 @@ 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) @@ -99,29 +97,6 @@ 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; @@ -156,19 +131,7 @@ namespace NzbDrone.Common.Disk if (index == -1) { - 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(null); } return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); @@ -177,8 +140,6 @@ namespace NzbDrone.Common.Disk public string FullPath => _path; - public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind); - public string FileName { get @@ -201,30 +162,7 @@ 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); + public bool IsValid => _path.IsPathValid(); private int GetFileNameIndex() { @@ -253,50 +191,11 @@ 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; @@ -357,7 +256,7 @@ namespace NzbDrone.Common.Disk var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; - for (var i = 0; i < leftFragments.Length; i++) + for (int i = 0; i < leftFragments.Length; i++) { if (!string.Equals(leftFragments[i], rightFragments[i], stringComparison)) { @@ -369,11 +268,6 @@ namespace NzbDrone.Common.Disk } public bool Equals(OsPath other) - { - return Equals(other, false); - } - - public bool Equals(OsPath other, bool ignoreTrailingSlash) { if (ReferenceEquals(other, null)) { @@ -385,8 +279,8 @@ namespace NzbDrone.Common.Disk return true; } - var left = ignoreTrailingSlash ? PathWithoutTrailingSlash : _path; - var right = ignoreTrailingSlash ? other.PathWithoutTrailingSlash : other._path; + var left = _path; + var right = other._path; if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) { @@ -479,12 +373,12 @@ namespace NzbDrone.Common.Disk var newFragments = new List(); - for (var j = i; j < rightFragments.Length; j++) + for (int j = i; j < rightFragments.Length; j++) { newFragments.Add(".."); } - for (var j = i; j < leftFragments.Length; j++) + for (int j = i; j < leftFragments.Length; j++) { newFragments.Add(leftFragments[j]); } diff --git a/src/NzbDrone.Common/Disk/PathValidationType.cs b/src/NzbDrone.Common/Disk/PathValidationType.cs deleted file mode 100644 index 395e3f0d8..000000000 --- a/src/NzbDrone.Common/Disk/PathValidationType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Common.Disk -{ - public enum PathValidationType - { - CurrentOs, - AnyOs - } -} diff --git a/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs b/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs index ba02ee7f6..1c10c9f28 100644 --- a/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs +++ b/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Text.RegularExpressions; -using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat.Resources; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -112,14 +111,14 @@ namespace NzbDrone.Common.EnsureThat } [DebuggerStepThrough] - public static Param IsValidPath(this Param param, PathValidationType validationType) + public static Param IsValidPath(this Param param) { if (string.IsNullOrWhiteSpace(param.Value)) { throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace); } - if (param.Value.IsPathValid(validationType)) + if (param.Value.IsPathValid()) { return param; } diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs index 178ce7a0f..5f0730d09 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Exceptions; @@ -159,7 +161,7 @@ namespace NzbDrone.Common.EnvironmentInfo private void CleanupSqLiteRollbackFiles() { - _diskProvider.GetFiles(_appFolderInfo.AppDataFolder, false) + _diskProvider.GetFiles(_appFolderInfo.AppDataFolder, SearchOption.TopDirectoryOnly) .Where(f => Path.GetFileName(f).StartsWith("nzbdrone.db")) .ToList() .ForEach(_diskProvider.DeleteFile); diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index aece27859..23b3ab885 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using NLog; @@ -24,25 +25,22 @@ namespace NzbDrone.Common.EnvironmentInfo static OsInfo() { - if (OperatingSystem.IsWindows()) + var platform = Environment.OSVersion.Platform; + + switch (platform) { - 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 + case PlatformID.Win32NT: + { + Os = Os.Windows; + break; + } + + case PlatformID.MacOSX: + case PlatformID.Unix: + { + Os = GetPosixFlavour(); + break; + } } } @@ -79,13 +77,64 @@ namespace NzbDrone.Common.EnvironmentInfo FullName = Name; } - if (IsLinux && - (File.Exists("/.dockerenv") || - (File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")))) + if (IsLinux && File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/")) { 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/DateTimeExtensions.cs b/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs index 69e2e5e17..57f8aac71 100644 --- a/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs +++ b/src/NzbDrone.Common/Extensions/DateTimeExtensions.cs @@ -36,14 +36,14 @@ namespace NzbDrone.Common.Extensions public static bool IsValidDate(this string dateTime) { - DateTime.TryParse(dateTime, out var result); + DateTime.TryParse(dateTime, out DateTime result); return !result.Equals(default(DateTime)); } public static bool IsFutureDate(this string dateTime) { - DateTime.TryParse(dateTime, out var result); + DateTime.TryParse(dateTime, out DateTime result); return !result.Equals(default(DateTime)) && result.After(DateTime.Now); } diff --git a/src/NzbDrone.Common/Extensions/EnumExtensions.cs b/src/NzbDrone.Common/Extensions/EnumExtensions.cs deleted file mode 100644 index fcc550d99..000000000 --- a/src/NzbDrone.Common/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Linq; - -namespace NzbDrone.Common.Extensions -{ - public static class EnumExtensions - { - public static T GetAttribute(this Enum value) - where T : Attribute - { - var enumType = value.GetType(); - var name = Enum.GetName(enumType, value); - - return name == null ? null : enumType.GetField(name)?.GetCustomAttributes(false).OfType().SingleOrDefault(); - } - } -} diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index 9e5385593..d18e58e51 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -126,9 +126,9 @@ namespace NzbDrone.Common.Extensions private static IEnumerable InternalDropLast(IEnumerable source, int n) { - var buffer = new Queue(n + 1); + Queue buffer = new Queue(n + 1); - foreach (var x in source) + foreach (T x in source) { buffer.Enqueue(x); diff --git a/src/NzbDrone.Common/Extensions/NumberExtensions.cs b/src/NzbDrone.Common/Extensions/Int64Extensions.cs similarity index 53% rename from src/NzbDrone.Common/Extensions/NumberExtensions.cs rename to src/NzbDrone.Common/Extensions/Int64Extensions.cs index 15037b20b..bfca7f66c 100644 --- a/src/NzbDrone.Common/Extensions/NumberExtensions.cs +++ b/src/NzbDrone.Common/Extensions/Int64Extensions.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Globalization; namespace NzbDrone.Common.Extensions { - public static class NumberExtensions + public static class Int64Extensions { private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; @@ -26,25 +26,5 @@ 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/IpAddressExtensions.cs b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs index cbc1f5f83..7feb431c4 100644 --- a/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IpAddressExtensions.cs @@ -39,24 +39,18 @@ 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) - var isLinkLocal = ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; + bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254; // Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) - var isClassA = ipv4Bytes[0] == 10; + bool IsClassA() => ipv4Bytes[0] == 10; // Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12) - var isClassB = ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31; + bool 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) - var isClassC = ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; + bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; - 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; + return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); } } } diff --git a/src/NzbDrone.Common/Extensions/LevenstheinExtensions.cs b/src/NzbDrone.Common/Extensions/LevenstheinExtensions.cs index eb20ce7b0..825525457 100644 --- a/src/NzbDrone.Common/Extensions/LevenstheinExtensions.cs +++ b/src/NzbDrone.Common/Extensions/LevenstheinExtensions.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Common.Extensions return text.Length * costDelete; } - var matrix = new int[other.Length + 1]; + int[] matrix = new int[other.Length + 1]; for (var i = 1; i < matrix.Length; i++) { @@ -30,13 +30,13 @@ namespace NzbDrone.Common.Extensions for (var i = 0; i < text.Length; i++) { - var topLeft = matrix[0]; + int topLeft = matrix[0]; matrix[0] = matrix[0] + costDelete; for (var j = 0; j < other.Length; j++) { - var top = matrix[j]; - var left = matrix[j + 1]; + int top = matrix[j]; + int left = matrix[j + 1]; var sumIns = top + costInsert; var sumDel = left + costDelete; diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 30a467f21..cf82e601c 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; @@ -25,26 +24,22 @@ 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); + Ensure.That(path, () => path).IsValidPath(); var info = new FileInfo(path.Trim()); // UNC - if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\")) + if (OsInfo.IsWindows && 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) @@ -85,50 +80,55 @@ namespace NzbDrone.Common.Extensions throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); } - return childPath.Substring(parentPath.Length).Trim('\\', '/'); + return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); } public static string GetParentPath(this string childPath) { - var path = new OsPath(childPath).Directory; + var cleanPath = OsInfo.IsWindows + ? PARENT_PATH_END_SLASH_REGEX.Replace(childPath, "") + : childPath.TrimEnd(Path.DirectorySeparatorChar); - return path == OsPath.Null ? null : path.PathWithoutTrailingSlash; - } + if (cleanPath.IsNullOrWhiteSpace()) + { + return null; + } - 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; + return Directory.GetParent(cleanPath)?.FullName; } public static string GetCleanPath(this string path) { - var osPath = new OsPath(path); + var cleanPath = OsInfo.IsWindows + ? PARENT_PATH_END_SLASH_REGEX.Replace(path, "") + : path.TrimEnd(Path.DirectorySeparatorChar); - return osPath == OsPath.Null ? null : osPath.PathWithoutTrailingSlash; + return cleanPath; } public static bool IsParentPath(this string parentPath, string childPath) { - var parent = new OsPath(parentPath); - var child = new OsPath(childPath); - - while (child.Directory != OsPath.Null) + if (parentPath != "/" && !parentPath.EndsWith(":\\")) { - if (child.Directory.Equals(parent, true)) + parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); + } + + if (childPath != "/" && !parentPath.EndsWith(":\\")) + { + 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)) { return true; } - child = child.Directory; + child = child.Parent; } return false; @@ -136,54 +136,28 @@ namespace NzbDrone.Common.Extensions private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled); - public static bool IsPathValid(this string path, PathValidationType validationType) + public static bool IsPathValid(this string path) { - if (string.IsNullOrWhiteSpace(path) || path.ContainsInvalidPathChars()) + if (path.ContainsInvalidPathChars() || string.IsNullOrWhiteSpace(path)) { 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); - } - if (OsInfo.IsNotWindows) { - return IsPathValidForNonWindows(path); + return path.StartsWith(Path.DirectorySeparatorChar.ToString()); } - return IsPathValidForWindows(path); + if (path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path)) + { + return true; + } + + return false; } public static bool ContainsInvalidPathChars(this string text) { - if (text.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(text)); - } - return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; } @@ -274,11 +248,6 @@ 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; @@ -368,15 +337,5 @@ namespace NzbDrone.Common.Extensions { return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE); } - - private static bool IsPathValidForWindows(string path) - { - return path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path); - } - - private static bool IsPathValidForNonWindows(string path) - { - return path.StartsWith("/"); - } } } diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index b36f81c40..495bfc6ac 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -198,13 +198,13 @@ namespace NzbDrone.Common.Extensions public static string CleanFileName(this string name) { - var result = name; + string result = name; string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" }; result = result.Replace(": ", " - "); - for (var i = 0; i < badCharacters.Length; i++) + for (int i = 0; i < badCharacters.Length; i++) { result = result.Replace(badCharacters[i], goodCharacters[i]); } diff --git a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs index 3b9f3db49..1ed79c319 100644 --- a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs +++ b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs @@ -6,7 +6,9 @@ namespace NzbDrone.Common.Extensions { public static int? ParseInt32(this string source) { - if (int.TryParse(source, out var result)) + int result; + + if (int.TryParse(source, out result)) { return result; } @@ -16,7 +18,9 @@ namespace NzbDrone.Common.Extensions public static long? ParseInt64(this string source) { - if (long.TryParse(source, out var result)) + long result; + + if (long.TryParse(source, out result)) { return result; } @@ -26,7 +30,9 @@ namespace NzbDrone.Common.Extensions public static double? ParseDouble(this string source) { - if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out var result)) + double result; + + if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result)) { return result; } diff --git a/src/NzbDrone.Common/Extensions/UrlExtensions.cs b/src/NzbDrone.Common/Extensions/UrlExtensions.cs index fbe1832a8..d71cfec15 100644 --- a/src/NzbDrone.Common/Extensions/UrlExtensions.cs +++ b/src/NzbDrone.Common/Extensions/UrlExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Web; namespace NzbDrone.Common.Extensions { @@ -19,24 +18,5 @@ 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/HashUtil.cs b/src/NzbDrone.Common/HashUtil.cs index 372b875a0..8817a95f8 100644 --- a/src/NzbDrone.Common/HashUtil.cs +++ b/src/NzbDrone.Common/HashUtil.cs @@ -8,9 +8,9 @@ namespace NzbDrone.Common { public static string CalculateCrc(string input) { - var mCrc = 0xffffffff; - var bytes = Encoding.UTF8.GetBytes(input); - foreach (var myByte in bytes) + uint mCrc = 0xffffffff; + byte[] bytes = Encoding.UTF8.GetBytes(input); + foreach (byte myByte in bytes) { mCrc ^= (uint)myByte << 24; for (var i = 0; i < 8; i++) @@ -29,14 +29,6 @@ namespace NzbDrone.Common return $"{mCrc:x8}"; } - public static string ComputeSha256Hash(string rawData) - { - using var sha256Hash = SHA256.Create(); - var hashBytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData)); - - return Convert.ToHexString(hashBytes); - } - public static string CalculateMd5(string s) { // Use input string to calculate MD5 hash diff --git a/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs b/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs index 54f3e2a8d..187c1fd43 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ICertificateValidationService.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 29bb49fa5..b34949879 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -4,7 +4,6 @@ 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; @@ -32,14 +31,11 @@ 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, - Logger logger) + ICacheManager cacheManager) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; @@ -48,17 +44,11 @@ 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) { - var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url) - { - Version = HttpVersion.Version20, - VersionPolicy = HttpVersionPolicy.RequestVersionOrLower - }; + var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url); requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent)); requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive; @@ -115,59 +105,52 @@ namespace NzbDrone.Common.Http.Dispatchers sw.Start(); - try + using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); { - using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); + byte[] data = null; + + try { - byte[] data = null; - - try + if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) { - if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) - { - await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token); - } - else - { - data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token); - } + await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token); } - catch (Exception ex) + else { - throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null); + data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult(); } - - var headers = responseMessage.Headers.ToNameValueCollection(); - - headers.Add(responseMessage.Content.Headers.ToNameValueCollection()); - - var responseCookies = new CookieContainer(); - - if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)) - { - foreach (var responseCookieHeader in cookieHeaders) - { - try - { - cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader); - } - catch - { - // Ignore invalid cookies - } - } - } - - var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri); - - sw.Stop(); - - return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode, responseMessage.Version); } - } - catch (OperationCanceledException ex) when (cts.IsCancellationRequested) - { - throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null); + catch (Exception ex) + { + throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null); + } + + var headers = responseMessage.Headers.ToNameValueCollection(); + + headers.Add(responseMessage.Content.Headers.ToNameValueCollection()); + + CookieContainer responseCookies = new CookieContainer(); + + if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)) + { + foreach (var responseCookieHeader in cookieHeaders) + { + try + { + cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader); + } + catch + { + // Ignore invalid cookies + } + } + } + + var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri); + + sw.Stop(); + + return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode); } } @@ -205,8 +188,6 @@ namespace NzbDrone.Common.Http.Dispatchers var client = new System.Net.Http.HttpClient(handler) { - DefaultRequestVersion = HttpVersion.Version20, - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower, Timeout = Timeout.InfiniteTimeSpan }; @@ -266,7 +247,7 @@ namespace NzbDrone.Common.Http.Dispatchers } } - private static void AddContentHeader(HttpRequestMessage request, string header, string value) + private void AddContentHeader(HttpRequestMessage request, string header, string value) { var headers = request.Content?.Headers; if (headers == null) @@ -283,27 +264,7 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } - 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) + private static 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. @@ -326,10 +287,10 @@ namespace NzbDrone.Common.Http.Dispatchers } catch { - // 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; + // 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; } finally { diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 326c30b04..b0c3acb13 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -91,7 +91,6 @@ namespace NzbDrone.Common.Http { request.Method = HttpMethod.Get; request.ContentData = null; - request.ContentSummary = null; } // Save to add to final response @@ -109,7 +108,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.RedirectUrl); + _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); } if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode))) @@ -221,18 +220,11 @@ namespace NzbDrone.Common.Http }; } - try - { - sourceContainer.Add((Uri)request.Url, cookie); + sourceContainer.Add((Uri)request.Url, cookie); - if (request.StoreRequestCookie) - { - presistentContainer.Add((Uri)request.Url, cookie); - } - } - catch (CookieException ex) + if (request.StoreRequestCookie) { - _logger.Debug(ex, "Invalid cookie in {0}", (Uri)request.Url); + presistentContainer.Add((Uri)request.Url, cookie); } } } @@ -267,14 +259,7 @@ namespace NzbDrone.Common.Http }; } - try - { - sourceContainer.Add((Uri)request.Url, cookie); - } - catch (CookieException ex) - { - _logger.Debug(ex, "Invalid cookie in {0}", (Uri)request.Url); - } + sourceContainer.Add((Uri)request.Url, cookie); } } @@ -337,12 +322,11 @@ namespace NzbDrone.Common.Http _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); var stopWatch = Stopwatch.StartNew(); - await using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite)) + using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite)) { var request = new HttpRequest(url); request.AllowAutoRedirect = true; request.ResponseStream = fileStream; - request.RequestTimeout = TimeSpan.FromSeconds(300); var response = await GetAsync(request); if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html")) diff --git a/src/NzbDrone.Common/Http/HttpHeader.cs b/src/NzbDrone.Common/Http/HttpHeader.cs index aff33b2dd..020c04d93 100644 --- a/src/NzbDrone.Common/Http/HttpHeader.cs +++ b/src/NzbDrone.Common/Http/HttpHeader.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; +using System.Net; using System.Net.Http.Headers; using System.Text; using NzbDrone.Common.Extensions; diff --git a/src/NzbDrone.Common/Http/HttpResponse.cs b/src/NzbDrone.Common/Http/HttpResponse.cs index 310955224..a6c9f7f86 100644 --- a/src/NzbDrone.Common/Http/HttpResponse.cs +++ b/src/NzbDrone.Common/Http/HttpResponse.cs @@ -9,9 +9,9 @@ namespace NzbDrone.Common.Http { public class HttpResponse { - private static readonly Regex RegexRefresh = new ("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled); + private static readonly Regex RegexRefresh = new Regex("^(.*?url)=(.*?)(?:;|$)", RegexOptions.Compiled); - public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null) + public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK) { Request = request; Headers = headers; @@ -19,10 +19,9 @@ namespace NzbDrone.Common.Http ResponseData = binaryData; StatusCode = statusCode; ElapsedTime = elapsedTime; - Version = version; } - public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null) + public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK) { Request = request; Headers = headers; @@ -31,7 +30,6 @@ namespace NzbDrone.Common.Http _content = content; StatusCode = statusCode; ElapsedTime = elapsedTime; - Version = version; } public HttpRequest Request { get; private set; } @@ -39,7 +37,6 @@ namespace NzbDrone.Common.Http public CookieCollection Cookies { get; private set; } public HttpStatusCode StatusCode { get; private set; } public long ElapsedTime { get; private set; } - public Version Version { get; private set; } public byte[] ResponseData { get; private set; } private string _content; @@ -66,8 +63,6 @@ namespace NzbDrone.Common.Http public bool HasHttpError => (int)StatusCode >= 400; - public bool HasHttpServerError => (int)StatusCode >= 500; - public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved || StatusCode == HttpStatusCode.Found || StatusCode == HttpStatusCode.SeeOther || @@ -124,7 +119,7 @@ namespace NzbDrone.Common.Http public override string ToString() { - var result = $"Res: HTTP/{Version} [{Request.Method}] {Request.Url}: {(int)StatusCode}.{StatusCode} ({ResponseData?.Length ?? 0} bytes)"; + var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0); if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) { @@ -139,7 +134,7 @@ namespace NzbDrone.Common.Http where T : new() { public HttpResponse(HttpResponse response) - : base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode, response.Version) + : base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.ElapsedTime, response.StatusCode) { Resource = Json.Deserialize(response.Content); } diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs index 2277ed60d..45d2b98b5 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http public HttpUri(string scheme, string host, int? port, string path, string query, string fragment) { - var builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(); if (scheme.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs index c80044d29..ef59c9c0f 100644 --- a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs +++ b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs @@ -30,9 +30,8 @@ namespace NzbDrone.Common.Http.Proxy { if (!string.IsNullOrWhiteSpace(BypassFilter)) { - var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - for (var i = 0; i < hostlist.Length; i++) + var hostlist = BypassFilter.Split(','); + for (int 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 e7ab0126d..e03161702 100644 --- a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs @@ -92,10 +92,6 @@ 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 393d6613a..fc74a68e0 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -9,65 +9,61 @@ namespace NzbDrone.Common.Instrumentation { private static readonly Regex[] CleansingRules = { - // Url - 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), - new (@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"/fetch/[a-z0-9]{32}/(?[a-z0-9]{32})", RegexOptions.Compiled), - new (@"getnzb.*?(?<=\?|&)(r)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"\b(\w*)?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - 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), + // Url + new Regex(@"(?<=[?&: ;])(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 Regex(@"(?<=[?& ;])[^=]*?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"rss\.torrentleech\.org/(?!rss)(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"/fetch/[a-z0-9]{32}/(?[a-z0-9]{32})", RegexOptions.Compiled), + new Regex(@"getnzb.*?(?<=\?|&)(r)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\b(\w*)?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?<=authkey = "")(?[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // UNIT3D - new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // UNIT3D + new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Path - new (@"""C:\\Users\\(?[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"""/(home|Users)/(?[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // Path + new Regex(@"""C:\\Users\\(?[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"""/home/(?[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory - new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory + new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce"), - // NzbGet - new (@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // NzbGet + new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Sabnzbd - new (@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"""email_(account|to|from|pwd)""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // Sabnzbd + new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // uTorrent - new (@"\[""[a-z._]*(username|password)"",\d,""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // uTorrent + new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Deluge - new (@"auth.login\(""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // Deluge + new Regex(@"auth.login\(""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV) - new (@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"getTorrents\(""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // BroadcastheNet (;torrent_pass|torrents_notify_ is for MTV) + new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"getTorrents\(""(?[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?<=\?|&|;|=)(authkey|torrent_pass|torrents_notify)[_=](?[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Plex - new (@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + // Plex + new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - // Indexer Responses - new (@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?[^&=]+?)/(?[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"""(info_hash|token|((pass|rss)[- _]?key))"":""(?[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - - // Applications - new (@"""name"":""apikey"",""value"":""(?[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), - - // Discord - new (@"discord.com/api/webhooks/((?[\w-]+)/)?(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase) + // Indexer Responses + new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?[^&=]+?)/(?[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@",""info_hash"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"""token"":""(?[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@",""pass[- _]?key"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@",""rss[- _]?key"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), }; - private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(? - { - var value = m.Value; - foreach (var capture in m.Groups["secret"].Captures.OfType().Reverse()) { - value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)"); - } + var value = m.Value; + foreach (var capture in m.Groups["secret"].Captures.OfType().Reverse()) + { + value = value.Replace(capture.Index - m.Index, capture.Length, "(removed)"); + } - return value; - }); + return value; + }); } message = CleanseRemoteIPRegex.Replace(message, CleanseRemoteIP); diff --git a/src/NzbDrone.Common/Instrumentation/CleansingClefLogLayout.cs b/src/NzbDrone.Common/Instrumentation/CleansingClefLogLayout.cs deleted file mode 100644 index f110b96ac..000000000 --- a/src/NzbDrone.Common/Instrumentation/CleansingClefLogLayout.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text; -using NLog; -using NLog.Layouts.ClefJsonLayout; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Common.Instrumentation; - -public class CleansingClefLogLayout : CompactJsonLayout -{ - protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) - { - base.RenderFormattedMessage(logEvent, target); - - if (RuntimeInfo.IsProduction) - { - var result = CleanseLogMessage.Cleanse(target.ToString()); - target.Clear(); - target.Append(result); - } - } -} diff --git a/src/NzbDrone.Common/Instrumentation/CleansingConsoleLogLayout.cs b/src/NzbDrone.Common/Instrumentation/CleansingConsoleLogLayout.cs deleted file mode 100644 index f894a4df5..000000000 --- a/src/NzbDrone.Common/Instrumentation/CleansingConsoleLogLayout.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Text; -using NLog; -using NLog.Layouts; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Common.Instrumentation; - -public class CleansingConsoleLogLayout : SimpleLayout -{ - public CleansingConsoleLogLayout(string format) - : base(format) - { - } - - protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) - { - base.RenderFormattedMessage(logEvent, target); - - if (RuntimeInfo.IsProduction) - { - var result = CleanseLogMessage.Cleanse(target.ToString()); - target.Clear(); - target.Append(result); - } - } -} diff --git a/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs index 34df2dff3..1e32d399f 100644 --- a/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs +++ b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using NzbDrone.Common.Serializer; namespace NzbDrone.Common.Instrumentation @@ -16,7 +16,7 @@ namespace NzbDrone.Common.Instrumentation } } - foreach (var token in json) + foreach (JToken token in json) { Visit(token); } diff --git a/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs index 68201d62c..437288d69 100644 --- a/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs +++ b/src/NzbDrone.Common/Instrumentation/Extensions/SentryLoggerExtensions.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using NLog; +using NLog.Fluent; namespace NzbDrone.Common.Instrumentation.Extensions { diff --git a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs index bd4d2c187..50ede78a5 100644 --- a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs +++ b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using NLog; +using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Common.Instrumentation { diff --git a/src/NzbDrone.Common/Instrumentation/CleansingFileTarget.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs similarity index 84% rename from src/NzbDrone.Common/Instrumentation/CleansingFileTarget.cs rename to src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs index f74d1fca4..e2c38f95c 100644 --- a/src/NzbDrone.Common/Instrumentation/CleansingFileTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs @@ -1,10 +1,11 @@ +using System; using System.Text; using NLog; using NLog.Targets; namespace NzbDrone.Common.Instrumentation { - public class CleansingFileTarget : FileTarget + public class NzbDroneFileTarget : FileTarget { protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) { diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index d9fdd5b25..7d8cfaf83 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -12,11 +12,7 @@ namespace NzbDrone.Common.Instrumentation { public static class NzbDroneLogger { - private const string FileLogLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; - private const string ConsoleFormat = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; - - private static readonly CleansingConsoleLogLayout CleansingConsoleLayout = new (ConsoleFormat); - private static readonly CleansingClefLogLayout ClefLogLayout = new (); + private const string FILE_LOG_LAYOUT = @"${date:format=yyyy-MM-dd HH\:mm\:ss.f}|${level}|${logger}|${message}${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; private static bool _isConfigured; @@ -45,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation RegisterDebugger(); } - RegisterSentry(updateApp, appFolderInfo); + RegisterSentry(updateApp); if (updateApp) { @@ -66,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation LogManager.ReconfigExistingLoggers(); } - private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo) + private static void RegisterSentry(bool updateClient) { string dsn; @@ -77,11 +73,11 @@ namespace NzbDrone.Common.Instrumentation else { dsn = RuntimeInfo.IsProduction - ? "https://a1fa00bd1d60465ebd9aca58c5a22d00@sentry.servarr.com/27" + ? "https://d62a0313c35f4afc932b4a20e1072793@sentry.servarr.com/27" : "https://e38306161ff945999adf774a16e933c3@sentry.servarr.com/30"; } - var target = new SentryTarget(dsn, appFolderInfo) + var target = new SentryTarget(dsn) { Name = "sentryTarget", Layout = "${message}" @@ -98,7 +94,7 @@ namespace NzbDrone.Common.Instrumentation private static void RegisterDebugger() { - var target = new DebuggerTarget(); + DebuggerTarget target = new DebuggerTarget(); target.Name = "debuggerLogger"; target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; @@ -108,6 +104,16 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration.LoggingRules.Add(loggingRule); } + private static void RegisterGlobalFilters() + { + LogManager.Setup().LoadConfiguration(c => + { + c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); + c.ForLogger("System*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn); + }); + } + private static void RegisterConsole() { var level = LogLevel.Trace; @@ -115,12 +121,7 @@ namespace NzbDrone.Common.Instrumentation var coloredConsoleTarget = new ColoredConsoleTarget(); coloredConsoleTarget.Name = "consoleLogger"; - - var logFormat = Enum.TryParse(Environment.GetEnvironmentVariable("PROWLARR__LOG__CONSOLEFORMAT"), out var formatEnumValue) - ? formatEnumValue - : ConsoleLogFormat.Standard; - - ConfigureConsoleLayout(coloredConsoleTarget, logFormat); + coloredConsoleTarget.Layout = "[${level}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}"; var loggingRule = new LoggingRule("*", level, coloredConsoleTarget); @@ -137,7 +138,7 @@ namespace NzbDrone.Common.Instrumentation private static void RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel) { - var fileTarget = new CleansingFileTarget(); + var fileTarget = new NzbDroneFileTarget(); fileTarget.Name = name; fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), fileName); @@ -146,11 +147,11 @@ namespace NzbDrone.Common.Instrumentation fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttempts = 10; - fileTarget.ArchiveAboveSize = 1.Megabytes(); + fileTarget.ArchiveAboveSize = 1024000; fileTarget.MaxArchiveFiles = maxArchiveFiles; fileTarget.EnableFileDelete = true; fileTarget.ArchiveNumbering = ArchiveNumberingMode.Rolling; - fileTarget.Layout = FileLogLayout; + fileTarget.Layout = FILE_LOG_LAYOUT; var loggingRule = new LoggingRule("*", minLogLevel, fileTarget); @@ -169,7 +170,7 @@ namespace NzbDrone.Common.Instrumentation fileTarget.ConcurrentWrites = false; fileTarget.ConcurrentWriteAttemptDelay = 50; fileTarget.ConcurrentWriteAttempts = 100; - fileTarget.Layout = FileLogLayout; + fileTarget.Layout = FILE_LOG_LAYOUT; var loggingRule = new LoggingRule("*", LogLevel.Trace, fileTarget); @@ -194,17 +195,6 @@ 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.", "")); @@ -214,20 +204,5 @@ 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/SentryCleanser.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryCleanser.cs index 85175b426..a56e57376 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryCleanser.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryCleanser.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Sentry; +using Sentry.Protocol; namespace NzbDrone.Common.Instrumentation.Sentry { diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 3a4737f74..5515b335c 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -12,6 +12,7 @@ using Npgsql; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using Sentry; +using Sentry.Protocol; namespace NzbDrone.Common.Instrumentation.Sentry { @@ -50,13 +51,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry "UnauthorizedAccessException", // Filter out people stuck in boot loops - "CorruptDatabaseException", - - // Filter SingleInstance Termination Exceptions - "TerminateApplicationException", - - // User config issue, root folder missing, etc. - "DirectoryNotFoundException" + "CorruptDatabaseException" }; public static readonly List FilteredExceptionMessages = new List @@ -106,41 +101,17 @@ namespace NzbDrone.Common.Instrumentation.Sentry public bool FilterEvents { get; set; } public bool SentryEnabled { get; set; } - public SentryTarget(string dsn, IAppFolderInfo appFolderInfo) + public SentryTarget(string dsn) { _sdk = SentrySdk.Init(o => { o.Dsn = dsn; o.AttachStacktrace = true; o.MaxBreadcrumbs = 200; - o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}"; - o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x)); - o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x)); + o.Release = BuildInfo.Release; + o.BeforeSend = x => SentryCleanser.CleanseEvent(x); + o.BeforeBreadcrumb = 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(); @@ -148,7 +119,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry _debounce = new SentryDebounce(); // initialize to true and reconfigure later - // Otherwise it will default to false and any errors occurring + // Otherwise it will default to false and any errors occuring // before config file gets read will not be filtered FilterEvents = true; SentryEnabled = true; @@ -158,7 +129,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry { SentrySdk.ConfigureScope(scope => { - scope.User = new SentryUser + scope.User = new User { Id = HashUtil.AnonymousToken() }; @@ -207,7 +178,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry private void OnError(Exception ex) { - if (ex is WebException webException) + var webException = ex as WebException; + + if (webException != null) { var response = webException.Response as HttpWebResponse; var statusCode = response?.StatusCode; @@ -339,21 +312,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry } } - var level = LoggingLevelMap[logEvent.Level]; var sentryEvent = new SentryEvent(logEvent.Exception) { - Level = level, + Level = LoggingLevelMap[logEvent.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/OAuth/OAuthRequest.cs b/src/NzbDrone.Common/OAuth/OAuthRequest.cs index ceb9b9117..cdf7d9e1f 100644 --- a/src/NzbDrone.Common/OAuth/OAuthRequest.cs +++ b/src/NzbDrone.Common/OAuth/OAuthRequest.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Common.OAuth public virtual string Version { get; set; } public virtual string SessionHandle { get; set; } - /// + /// public virtual string RequestUrl { get; set; } #if !WINRT diff --git a/src/NzbDrone.Common/Options/AppOptions.cs b/src/NzbDrone.Common/Options/AppOptions.cs deleted file mode 100644 index 74cdf1d29..000000000 --- a/src/NzbDrone.Common/Options/AppOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 64330b68b..000000000 --- a/src/NzbDrone.Common/Options/AuthOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 6460eeaa6..000000000 --- a/src/NzbDrone.Common/Options/LogOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index d21e12b2a..000000000 --- a/src/NzbDrone.Common/Options/ServerOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a8eaad8fb..000000000 --- a/src/NzbDrone.Common/Options/UpdateOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -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/PathEqualityComparer.cs b/src/NzbDrone.Common/PathEqualityComparer.cs index 5b9c3aa1c..d443a3bf0 100644 --- a/src/NzbDrone.Common/PathEqualityComparer.cs +++ b/src/NzbDrone.Common/PathEqualityComparer.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index c68207a09..796f4f6e8 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -6,7 +6,6 @@ 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; @@ -118,9 +117,7 @@ namespace NzbDrone.Common.Processes UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, - RedirectStandardInput = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 + RedirectStandardInput = true }; if (environmentVariables != null) @@ -134,7 +131,14 @@ namespace NzbDrone.Common.Processes var key = environmentVariable.Key.ToString(); var value = environmentVariable.Value?.ToString(); - startInfo.EnvironmentVariables[key] = value; + if (startInfo.EnvironmentVariables.ContainsKey(key)) + { + startInfo.EnvironmentVariables[key] = value; + } + else + { + startInfo.EnvironmentVariables.Add(key, value); + } } catch (Exception e) { @@ -316,7 +320,7 @@ namespace NzbDrone.Common.Processes processInfo = new ProcessInfo(); processInfo.Id = process.Id; processInfo.Name = process.ProcessName; - processInfo.StartPath = process.MainModule?.FileName; + processInfo.StartPath = GetExeFileName(process); if (process.Id != GetCurrentProcessId() && process.HasExited) { @@ -331,9 +335,28 @@ namespace NzbDrone.Common.Processes return processInfo; } + private static string GetExeFileName(Process process) + { + if (process.MainModule.FileName != "mono.exe") + { + return process.MainModule.FileName; + } + + return process.Modules.Cast().FirstOrDefault(module => module.ModuleName.ToLower().EndsWith(".exe")).FileName; + } + private List GetProcessesByName(string name) { - var processes = Process.GetProcessesByName(name).ToList(); + //TODO: move this to an OS specific class + var monoProcesses = Process.GetProcessesByName("mono") + .Union(Process.GetProcessesByName("mono-sgen")) + .Where(process => + process.Modules.Cast() + .Any(module => + module.ModuleName.ToLower() == name.ToLower() + ".exe")); + + var processes = Process.GetProcessesByName(name) + .Union(monoProcesses).ToList(); _logger.Debug("Found {0} processes with the name: {1}", processes.Count, name); diff --git a/src/NzbDrone.Common/Prowlarr.Common.csproj b/src/NzbDrone.Common/Prowlarr.Common.csproj index 106890399..e5475de4e 100644 --- a/src/NzbDrone.Common/Prowlarr.Common.csproj +++ b/src/NzbDrone.Common/Prowlarr.Common.csproj @@ -4,25 +4,22 @@ ISMUSL - - - - + + + - - - - - + + + + - - + - + - + diff --git a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs index fef5b3c94..fce2f295f 100644 --- a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs +++ b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/Json.cs @@ -121,11 +121,6 @@ namespace NzbDrone.Common.Serializer return JsonConvert.SerializeObject(obj, SerializerSettings); } - public static string ToJson(this object obj, Formatting formatting) - { - return JsonConvert.SerializeObject(obj, formatting, SerializerSettings); - } - public static void Serialize(TModel model, TextWriter outputStream) { var jsonTextWriter = new JsonTextWriter(outputStream); diff --git a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/JsonVisitor.cs b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/JsonVisitor.cs index 093de5b99..0e69a0ae0 100644 --- a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/JsonVisitor.cs +++ b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/JsonVisitor.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; namespace NzbDrone.Common.Serializer { @@ -60,7 +60,7 @@ namespace NzbDrone.Common.Serializer public virtual void Visit(JArray json) { - foreach (var token in json) + foreach (JToken token in json) { Visit(token); } @@ -72,7 +72,7 @@ namespace NzbDrone.Common.Serializer public virtual void Visit(JObject json) { - foreach (var property in json.Properties()) + foreach (JProperty property in json.Properties()) { Visit(property); } diff --git a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/UnderscoreStringEnumConverter.cs b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/UnderscoreStringEnumConverter.cs index b202253b6..772f5640c 100644 --- a/src/NzbDrone.Common/Serializer/Newtonsoft.Json/UnderscoreStringEnumConverter.cs +++ b/src/NzbDrone.Common/Serializer/Newtonsoft.Json/UnderscoreStringEnumConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using Newtonsoft.Json; @@ -42,7 +42,7 @@ namespace NzbDrone.Common.Serializer var enumText = value.ToString(); var builder = new StringBuilder(enumText.Length + 4); builder.Append(char.ToLower(enumText[0])); - for (var i = 1; i < enumText.Length; i++) + for (int i = 1; i < enumText.Length; i++) { if (char.IsUpper(enumText[i])) { diff --git a/src/NzbDrone.Common/Serializer/System.Text.Json/BooleanConverter.cs b/src/NzbDrone.Common/Serializer/System.Text.Json/BooleanConverter.cs deleted file mode 100644 index dc7af179b..000000000 --- a/src/NzbDrone.Common/Serializer/System.Text.Json/BooleanConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace NzbDrone.Common.Serializer; - -public class BooleanConverter : JsonConverter -{ - public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TokenType switch - { - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.Number => reader.GetInt64() switch - { - 1 => true, - 0 => false, - _ => throw new JsonException() - }, - _ => throw new JsonException() - }; - } - - public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) - { - writer.WriteBooleanValue(value); - } -} diff --git a/src/NzbDrone.Common/Serializer/System.Text.Json/STJVersionConverter.cs b/src/NzbDrone.Common/Serializer/System.Text.Json/STJVersionConverter.cs index 6fd024f7d..70ad492c3 100644 --- a/src/NzbDrone.Common/Serializer/System.Text.Json/STJVersionConverter.cs +++ b/src/NzbDrone.Common/Serializer/System.Text.Json/STJVersionConverter.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Common.Serializer { try { - var v = new Version(reader.GetString()); + Version v = new Version(reader.GetString()); return v; } catch (Exception) diff --git a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs index e799b678b..4fba4fae1 100644 --- a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs +++ b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs @@ -36,7 +36,6 @@ namespace NzbDrone.Common.Serializer serializerSettings.Converters.Add(new STJTimeSpanConverter()); serializerSettings.Converters.Add(new STJUtcConverter()); serializerSettings.Converters.Add(new DictionaryStringObjectConverter()); - serializerSettings.Converters.Add(new BooleanConverter()); } public static T Deserialize(string json) diff --git a/src/NzbDrone.Common/ServiceProvider.cs b/src/NzbDrone.Common/ServiceProvider.cs index b3552c623..d9a95192e 100644 --- a/src/NzbDrone.Common/ServiceProvider.cs +++ b/src/NzbDrone.Common/ServiceProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.ServiceProcess; using NLog; diff --git a/src/NzbDrone.Common/TPL/DebounceManager.cs b/src/NzbDrone.Common/TPL/DebounceManager.cs deleted file mode 100644 index 60803a3a9..000000000 --- a/src/NzbDrone.Common/TPL/DebounceManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace NzbDrone.Common.TPL -{ - public interface IDebounceManager - { - Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration); - } - - public class DebounceManager : IDebounceManager - { - public Debouncer CreateDebouncer(Action action, TimeSpan debounceDuration) - { - return new Debouncer(action, debounceDuration); - } - } -} diff --git a/src/NzbDrone.Common/TPL/Debouncer.cs b/src/NzbDrone.Common/TPL/Debouncer.cs index 7f8435961..0fa101525 100644 --- a/src/NzbDrone.Common/TPL/Debouncer.cs +++ b/src/NzbDrone.Common/TPL/Debouncer.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Common.TPL { public class Debouncer { - protected readonly Action _action; - protected readonly System.Timers.Timer _timer; + private readonly Action _action; + private readonly System.Timers.Timer _timer; - protected volatile int _paused; - protected volatile bool _triggered; + private volatile int _paused; + private volatile bool _triggered; public Debouncer(Action action, TimeSpan debounceDuration) { @@ -27,7 +27,7 @@ namespace NzbDrone.Common.TPL } } - public virtual void Execute() + public void Execute() { lock (_timer) { @@ -39,7 +39,7 @@ namespace NzbDrone.Common.TPL } } - public virtual void Pause() + public void Pause() { lock (_timer) { @@ -48,7 +48,7 @@ namespace NzbDrone.Common.TPL } } - public virtual void Resume() + public void Resume() { lock (_timer) { diff --git a/src/NzbDrone.Common/TPL/LimitedConcurrencyLevelTaskScheduler.cs b/src/NzbDrone.Common/TPL/LimitedConcurrencyLevelTaskScheduler.cs index ba9d804d3..a6137486b 100644 --- a/src/NzbDrone.Common/TPL/LimitedConcurrencyLevelTaskScheduler.cs +++ b/src/NzbDrone.Common/TPL/LimitedConcurrencyLevelTaskScheduler.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Common.TPL /// An enumerable of the tasks currently scheduled. protected sealed override IEnumerable GetScheduledTasks() { - var lockTaken = false; + bool lockTaken = false; try { Monitor.TryEnter(_tasks, ref lockTaken); diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 8f284d003..cff2fd254 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -110,7 +110,7 @@ namespace NzbDrone.Console } System.Console.WriteLine("Non-recoverable failure, waiting for user intervention..."); - for (var i = 0; i < 3600; i++) + for (int i = 0; i < 3600; i++) { System.Threading.Thread.Sleep(1000); if (!System.Console.IsInputRedirected && System.Console.KeyAvailable) diff --git a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs index 2369c9ddf..906f4d611 100644 --- a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Configuration [Test] public void Get_value_should_return_default_when_no_value() { - Subject.HistoryCleanupDays.Should().Be(30); + Subject.HistoryCleanupDays.Should().Be(365); } [Test] @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.Configuration public void get_value_with_out_persist_should_not_store_default_value() { var interval = Subject.HistoryCleanupDays; - interval.Should().Be(30); + interval.Should().Be(365); Mocker.GetMock().Verify(c => c.Insert(It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs b/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs index fac7e7f00..9f0cf4b92 100644 --- a/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Test.Datastore [TestFixture] public class BasicRepositoryFixture : DbTest, ScheduledTask> { - private readonly TimeSpan _dateTimePrecision = TimeSpan.FromMilliseconds(20); private List _basicList; [SetUp] @@ -21,7 +20,7 @@ namespace NzbDrone.Core.Test.Datastore { AssertionOptions.AssertEquivalencyUsing(options => { - options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime(), _dateTimePrecision)).WhenTypeIs(); + options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs(); return options; }); @@ -198,7 +197,7 @@ namespace NzbDrone.Core.Test.Datastore Subject.SetFields(_basicList, x => x.Interval); - for (var i = 0; i < _basicList.Count; i++) + for (int i = 0; i < _basicList.Count; i++) { _basicList[i].LastExecution = executionBackup[i]; } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs deleted file mode 100644 index 79d0adaee..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs +++ /dev/null @@ -1,43 +0,0 @@ -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/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index 7f8a157b0..985fbb7d5 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -27,20 +27,6 @@ namespace NzbDrone.Core.Test.Datastore Mocker.Resolve().Vacuum(); } - [Test] - public void postgres_should_not_contain_timestamp_without_timezone_columns() - { - if (Db.DatabaseType != DatabaseType.PostgreSQL) - { - return; - } - - Mocker.Resolve() - .OpenConnection().Query("SELECT table_name, column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'public' AND data_type = 'timestamp without time zone'") - .Should() - .BeNullOrEmpty(); - } - [Test] public void get_version() { diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs deleted file mode 100644 index 05bf04fea..000000000 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseVersionParserFixture.cs +++ /dev/null @@ -1,38 +0,0 @@ -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/031_apprise_server_urlFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/031_apprise_server_urlFixture.cs deleted file mode 100644 index 907bd7e47..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/031_apprise_server_urlFixture.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -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 apprise_server_urlFixture : MigrationTest - { - [Test] - public void should_rename_server_url_setting_for_apprise() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("Notifications").Row(new - { - Name = "Apprise", - Implementation = "Apprise", - Settings = new - { - BaseUrl = "http://localhost:8000", - NotificationType = 0 - }.ToJson(), - ConfigContract = "AppriseSettings", - OnHealthIssue = true, - IncludeHealthWarnings = true, - OnApplicationUpdate = true, - OnGrab = true, - IncludeManualGrabs = true - }); - }); - - var items = db.Query("SELECT * FROM \"Notifications\""); - - items.Should().HaveCount(1); - - items.First().Settings.Should().NotContainKey("baseUrl"); - items.First().Settings.Should().ContainKey("serverUrl"); - items.First().Settings.GetValueOrDefault("serverUrl").Should().Be("http://localhost:8000"); - } - } - - public class NotificationDefinition31 - { - public int Id { get; set; } - public int Priority { get; set; } - public string Name { get; set; } - public string Implementation { get; set; } - public Dictionary Settings { get; set; } - public string ConfigContract { get; set; } - public bool OnHealthIssue { get; set; } - public bool IncludeHealthWarnings { get; set; } - public bool OnApplicationUpdate { get; set; } - public bool OnGrab { get; set; } - public bool IncludeManualGrabs { get; set; } - public List Tags { get; set; } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/034_history_fix_data_titlesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/034_history_fix_data_titlesFixture.cs deleted file mode 100644 index 4c0a3393e..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/034_history_fix_data_titlesFixture.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -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 history_fix_data_titlesFixture : MigrationTest - { - [Test] - public void should_update_data_for_book_search() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("History").Row(new - { - IndexerId = 1, - Date = DateTime.UtcNow, - Data = new - { - Author = "Fake Author", - BookTitle = "Fake Book Title", - Publisher = "", - Year = "", - Genre = "", - Query = "", - QueryType = "book", - Source = "Prowlarr", - Host = "localhost" - }.ToJson(), - EventType = 2, - Successful = true - }); - }); - - var items = db.Query("SELECT * FROM \"History\""); - - items.Should().HaveCount(1); - - items.First().Data.Should().NotContainKey("bookTitle"); - items.First().Data.Should().ContainKey("title"); - items.First().Data.GetValueOrDefault("title").Should().Be("Fake Book Title"); - } - - [Test] - public void should_update_data_for_release_grabbed() - { - var db = WithMigrationTestDb(c => - { - c.Insert.IntoTable("History").Row(new - { - IndexerId = 1, - Date = DateTime.UtcNow, - Data = new - { - GrabMethod = "Proxy", - Title = "Fake Release Title", - Source = "Prowlarr", - Host = "localhost" - }.ToJson(), - EventType = 1, - Successful = true - }); - }); - - var items = db.Query("SELECT * FROM \"History\""); - - items.Should().HaveCount(1); - - items.First().Data.Should().NotContainKey("title"); - items.First().Data.Should().ContainKey("grabTitle"); - items.First().Data.GetValueOrDefault("grabTitle").Should().Be("Fake Release Title"); - } - } - - public class HistoryDefinition34 - { - public int Id { get; set; } - public int IndexerId { get; set; } - public DateTime Date { get; set; } - public Dictionary Data { get; set; } - public int EventType { get; set; } - public string DownloadId { get; set; } - public bool Successful { get; set; } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs deleted file mode 100644 index 513095162..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/039_email_encryptionFixture.cs +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index b51bf6433..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Migration/040_newznab_category_to_capabilities_settingsFixture.cs +++ /dev/null @@ -1,200 +0,0 @@ -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/FluentTest.cs b/src/NzbDrone.Core.Test/FluentTest.cs index 1747c44f2..a17f142e3 100644 --- a/src/NzbDrone.Core.Test/FluentTest.cs +++ b/src/NzbDrone.Core.Test/FluentTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using FluentAssertions; @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test [Test] public void ToBestDateTime_DayOfWeek() { - for (var i = 2; i < 7; i++) + for (int i = 2; i < 7; i++) { var dateTime = DateTime.Today.AddDays(i); diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index 3e6a8f66e..ef5202734 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -1,4 +1,5 @@ using System; +using Moq; using NUnit.Framework; using NzbDrone.Common.Cache; using NzbDrone.Common.Cloud; @@ -9,6 +10,7 @@ using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.TPL; using NzbDrone.Core.Configuration; using NzbDrone.Core.Http; +using NzbDrone.Core.Parser; using NzbDrone.Core.Security; using NzbDrone.Test.Common; @@ -25,7 +27,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(), TestLogger)); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve())); 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/NotificationStatusCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/NotificationStatusCheckFixture.cs deleted file mode 100644 index 67b79ae0b..000000000 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/NotificationStatusCheckFixture.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.HealthCheck.Checks; -using NzbDrone.Core.Localization; -using NzbDrone.Core.Notifications; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.HealthCheck.Checks -{ - [TestFixture] - public class NotificationStatusCheckFixture : CoreTest - { - private List _notifications = new List(); - private List _blockedNotifications = new List(); - - [SetUp] - public void SetUp() - { - Mocker.GetMock() - .Setup(v => v.GetAvailableProviders()) - .Returns(_notifications); - - Mocker.GetMock() - .Setup(v => v.GetBlockedProviders()) - .Returns(_blockedNotifications); - - Mocker.GetMock() - .Setup(s => s.GetLocalizedString(It.IsAny())) - .Returns("Some Warning Message"); - } - - private Mock GivenNotification(int id, double backoffHours, double failureHours) - { - var mockNotification = new Mock(); - mockNotification.SetupGet(s => s.Definition).Returns(new NotificationDefinition { Id = id }); - - _notifications.Add(mockNotification.Object); - - if (backoffHours != 0.0) - { - _blockedNotifications.Add(new NotificationStatus - { - ProviderId = id, - InitialFailure = DateTime.UtcNow.AddHours(-failureHours), - MostRecentFailure = DateTime.UtcNow.AddHours(-0.1), - EscalationLevel = 5, - DisabledTill = DateTime.UtcNow.AddHours(backoffHours) - }); - } - - return mockNotification; - } - - [Test] - public void should_not_return_error_when_no_notifications() - { - Subject.Check().ShouldBeOk(); - } - - [Test] - public void should_return_warning_if_notification_unavailable() - { - GivenNotification(1, 10.0, 24.0); - GivenNotification(2, 0.0, 0.0); - - Subject.Check().ShouldBeWarning(); - } - - [Test] - public void should_return_error_if_all_notifications_unavailable() - { - GivenNotification(1, 10.0, 24.0); - - Subject.Check().ShouldBeError(); - } - - [Test] - public void should_return_warning_if_few_notifications_unavailable() - { - GivenNotification(1, 10.0, 24.0); - GivenNotification(2, 10.0, 24.0); - GivenNotification(3, 0.0, 0.0); - - Subject.Check().ShouldBeWarning(); - } - } -} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs index 7d859eb9d..64eeb9169 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs @@ -7,7 +7,6 @@ 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 { @@ -22,10 +21,28 @@ 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() { - var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); + PosixOnly(); + + const string startupFolder = @"/opt/nzbdrone"; Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -45,8 +62,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_return_error_when_ui_folder_is_write_protected_and_update_automatically_is_enabled() { - var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); - var uiFolder = @"C:\NzbDrone\UI".AsOsAgnostic(); + PosixOnly(); + + const string startupFolder = @"/opt/nzbdrone"; + const string uiFolder = @"/opt/nzbdrone/UI"; Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -70,7 +89,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled() { - var startupFolder = @"C:\NzbDrone".AsOsAgnostic(); + PosixOnly(); Mocker.GetMock() .Setup(s => s.UpdateAutomatically) @@ -82,7 +101,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock() .Setup(s => s.StartUpFolder) - .Returns(startupFolder); + .Returns(@"/opt/nzbdrone"); Mocker.GetMock() .Verify(c => c.FolderWritable(It.IsAny()), Times.Never()); diff --git a/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs index 4ec0860ea..74802a39e 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs @@ -1,14 +1,10 @@ -using System; using System.Collections.Generic; using FluentAssertions; -using Moq; using NUnit.Framework; using NzbDrone.Common.Cache; using NzbDrone.Common.Messaging; -using NzbDrone.Common.TPL; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.HealthCheck { @@ -23,10 +19,10 @@ namespace NzbDrone.Core.Test.HealthCheck Mocker.SetConstant>(new[] { _healthCheck }); Mocker.SetConstant(Mocker.Resolve()); - Mocker.SetConstant(Mocker.Resolve()); - Mocker.GetMock().Setup(s => s.CreateDebouncer(It.IsAny(), It.IsAny())) - .Returns((a, t) => new MockDebouncer(a, t)); + Mocker.GetMock() + .Setup(v => v.GetServerChecks()) + .Returns(new List()); } [Test] diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs deleted file mode 100644 index 20e82ff7f..000000000 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatusFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Notifications; -using NzbDrone.Core.Notifications.Join; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Housekeeping.Housekeepers -{ - [TestFixture] - public class CleanupOrphanedNotificationStatusFixture : DbTest - { - private NotificationDefinition _notification; - - [SetUp] - public void Setup() - { - _notification = Builder.CreateNew() - .With(s => s.Settings = new JoinSettings { }) - .BuildNew(); - } - - private void GivenNotification() - { - Db.Insert(_notification); - } - - [Test] - public void should_delete_orphaned_notificationstatus() - { - var status = Builder.CreateNew() - .With(h => h.ProviderId = _notification.Id) - .BuildNew(); - Db.Insert(status); - - Subject.Clean(); - AllStoredModels.Should().BeEmpty(); - } - - [Test] - public void should_not_delete_unorphaned_notificationstatus() - { - GivenNotification(); - - var status = Builder.CreateNew() - .With(h => h.ProviderId = _notification.Id) - .BuildNew(); - Db.Insert(status); - - Subject.Clean(); - AllStoredModels.Should().HaveCount(1); - AllStoredModels.Should().Contain(h => h.ProviderId == _notification.Id); - } - } -} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs index a0f303609..39e193368 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); // BeCloseTo handles Postgres rounding times - AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime, TimeSpan.FromMilliseconds(20))); + AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime)); } } } diff --git a/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs b/src/NzbDrone.Core.Test/Http/HttpProxySettingsProviderFixture.cs index 2beeb16f9..067149904 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,172.16.0.0/12", true, null, null); + return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com", true, null, null); } [Test] @@ -23,7 +23,6 @@ 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] @@ -32,7 +31,6 @@ 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/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs deleted file mode 100644 index e67fda3f3..000000000 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.IndexerSearch; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.IndexerSearchTests -{ - public class ReleaseSearchServiceFixture : CoreTest - { - private Mock _mockIndexer; - - [SetUp] - public void SetUp() - { - _mockIndexer = Mocker.GetMock(); - _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 }); - _mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true); - - Mocker.GetMock() - .Setup(s => s.Enabled(It.IsAny())) - .Returns(new List { _mockIndexer.Object }); - } - - private List WatchForSearchCriteria() - { - var result = new List(); - - _mockIndexer.Setup(v => v.Fetch(It.IsAny())) - .Callback(s => result.Add(s)) - .Returns(Task.FromResult(new IndexerPageableQueryResult())); - - _mockIndexer.Setup(v => v.Fetch(It.IsAny())) - .Callback(s => result.Add(s)) - .Returns(Task.FromResult(new IndexerPageableQueryResult())); - - return result; - } - - [TestCase("tt0183790", "0183790")] - [TestCase("0183790", "0183790")] - [TestCase("183790", "0183790")] - [TestCase("tt10001870", "10001870")] - [TestCase("10001870", "10001870")] - public void should_normalize_imdbid_movie_search_criteria(string input, string expected) - { - var allCriteria = WatchForSearchCriteria(); - - var request = new NewznabRequest - { - t = "movie", - imdbid = input - }; - - Subject.Search(request, new List { 1 }, false); - - var criteria = allCriteria.OfType().ToList(); - - criteria.Count.Should().Be(1); - criteria[0].ImdbId.Should().Be(expected); - } - - [TestCase("tt0183790", "0183790")] - [TestCase("0183790", "0183790")] - [TestCase("183790", "0183790")] - [TestCase("tt10001870", "10001870")] - [TestCase("10001870", "10001870")] - public void should_normalize_imdbid_tv_search_criteria(string input, string expected) - { - var allCriteria = WatchForSearchCriteria(); - - var request = new NewznabRequest - { - t = "tvsearch", - imdbid = input - }; - - Subject.Search(request, new List { 1 }, false); - - var criteria = allCriteria.OfType().ToList(); - - criteria.Count.Should().Be(1); - criteria[0].ImdbId.Should().Be(expected); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs index a1e275725..433beda51 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/SearchDefinitionFixture.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/IndexerStatsTests/IndexerStatisticsServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerStatsTests/IndexerStatisticsServiceFixture.cs index b8f4ef702..c0ec172b4 100644 --- a/src/NzbDrone.Core.Test/IndexerStatsTests/IndexerStatisticsServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerStatsTests/IndexerStatisticsServiceFixture.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerStatsTests .Setup(o => o.Between(It.IsAny(), It.IsAny())) .Returns((s, f) => history); - var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow, new List { 5 }); + var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow); statistics.IndexerStatistics.Count.Should().Be(1); statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0); diff --git a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs index ae7eaa762..8532d7c8c 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/AnimeBytesTests/AnimeBytesFixture.cs @@ -43,12 +43,12 @@ namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests .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 { SearchTerm = "test", Categories = new[] { 2000, 5000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases; - releases.Should().HaveCount(39); + releases.Should().HaveCount(33); releases.First().Should().BeOfType(); - var firstTorrentInfo = releases.ElementAt(3) as TorrentInfo; + var firstTorrentInfo = releases.ElementAt(2) 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(20) as TorrentInfo; + var secondTorrentInfo = releases.ElementAt(16) 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(23) as TorrentInfo; + var thirdTorrentInfo = releases.ElementAt(18) 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(5) as TorrentInfo; + var fourthTorrentInfo = releases.ElementAt(3) 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(28) as TorrentInfo; + var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo; - fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS 2021 S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]"); + fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS 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(37) as TorrentInfo; + var sixthTorrentInfo = releases.ElementAt(31) 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/ExoticazFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/AvistazTests/ExoticazFixture.cs index fe4ada593..50cdcbac6 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 09:04:50")); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15: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/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs deleted file mode 100644 index 39d5a6f61..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.BroadcastheNet; -using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests -{ - public class BroadcastheNetRequestGeneratorFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Settings = new BroadcastheNetSettings - { - BaseUrl = "https://api.broadcasthe.net/", - ApiKey = "abc" - }; - - Subject.Capabilities = new IndexerCapabilities - { - LimitsDefault = 100, - LimitsMax = 1000, - TvSearchParams = new List - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId, TvSearchParam.RId - } - }; - } - - [Test] - public void should_have_empty_parameters_if_rss_search() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id } - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().BeNull(); - query.Tvrage.Should().BeNull(); - query.Search.Should().BeNullOrWhiteSpace(); - query.Category.Should().BeNullOrWhiteSpace(); - query.Name.Should().BeNullOrWhiteSpace(); - } - - [Test] - public void should_search_by_tvdbid_season_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - TvdbId = 371980, - Season = 1 - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(2); - - var firstPage = results.GetAllTiers().First().First(); - var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest); - - firstQuery.Tvdb.Should().Be("371980"); - firstQuery.Tvrage.Should().BeNull(); - firstQuery.Search.Should().BeNull(); - firstQuery.Category.Should().Be("Season"); - firstQuery.Name.Should().Be("Season 1%"); - - var secondPage = results.GetAllTiers().Skip(1).First().First(); - var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest); - - secondQuery.Tvdb.Should().Be("371980"); - secondQuery.Tvrage.Should().BeNull(); - secondQuery.Search.Should().BeNull(); - secondQuery.Category.Should().Be("Episode"); - secondQuery.Name.Should().Be("S01E%"); - } - - [Test] - public void should_search_by_tvdbid_season_episode_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - TvdbId = 371980, - Season = 1, - Episode = "3" - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().Be("371980"); - query.Tvrage.Should().BeNull(); - query.Search.Should().BeNull(); - query.Category.Should().Be("Episode"); - query.Name.Should().Be("S01E03%"); - } - - [Test] - public void should_search_by_tvdbid_daily_episode_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - TvdbId = 289574, - Season = 2023, - Episode = "01/03" - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().Be("289574"); - query.Tvrage.Should().BeNull(); - query.Search.Should().BeNull(); - query.Category.Should().Be("Episode"); - query.Name.Should().Be("2023.01.03"); - } - - [Test] - public void should_prefer_search_by_tvdbid_if_rid_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - TvdbId = 371980, - RId = 12345 - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().Be("371980"); - query.Tvrage.Should().BeNull(); - query.Search.Should().BeNull(); - query.Category.Should().BeNull(); - query.Name.Should().BeNull(); - } - - [Test] - public void should_search_by_term_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - SearchTerm = "Malcolm in the Middle" - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().BeNull(); - query.Tvrage.Should().BeNull(); - query.Search.Should().Be("Malcolm%in%the%Middle"); - query.Category.Should().BeNull(); - query.Name.Should().BeNull(); - } - - [Test] - public void should_search_by_term_season_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - SearchTerm = "Malcolm in the Middle", - Season = 2 - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(2); - - var firstPage = results.GetAllTiers().First().First(); - var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest); - - firstQuery.Tvdb.Should().BeNull(); - firstQuery.Tvrage.Should().BeNull(); - firstQuery.Search.Should().Be("Malcolm%in%the%Middle"); - firstQuery.Category.Should().Be("Season"); - firstQuery.Name.Should().Be("Season 2%"); - - var secondPage = results.GetAllTiers().Skip(1).First().First(); - var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest); - - secondQuery.Tvdb.Should().BeNull(); - secondQuery.Tvrage.Should().BeNull(); - secondQuery.Search.Should().Be("Malcolm%in%the%Middle"); - secondQuery.Category.Should().Be("Episode"); - secondQuery.Name.Should().Be("S02E%"); - } - - [Test] - public void should_search_by_term_season_episode_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - SearchTerm = "Malcolm in the Middle", - Season = 2, - Episode = "3" - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().BeNull(); - query.Tvrage.Should().BeNull(); - query.Search.Should().Be("Malcolm%in%the%Middle"); - query.Category.Should().Be("Episode"); - query.Name.Should().Be("S02E03%"); - } - - [Test] - public void should_search_by_term_daily_episode_if_supported() - { - var tvSearchCriteria = new TvSearchCriteria - { - Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }, - SearchTerm = "The Late Show with Stephen Colbert", - Season = 2023, - Episode = "01/03" - }; - - var results = Subject.GetSearchRequests(tvSearchCriteria); - - results.Tiers.Should().Be(1); - results.GetAllTiers().Should().HaveCount(1); - - var page = results.GetAllTiers().First().First(); - var query = ParseTorrentQueryFromRequest(page.HttpRequest); - - query.Tvdb.Should().BeNull(); - query.Tvrage.Should().BeNull(); - query.Search.Should().Be("The%Late%Show%with%Stephen%Colbert"); - query.Category.Should().Be("Episode"); - query.Name.Should().Be("2023.01.03"); - } - - private static BroadcastheNetTorrentQuery ParseTorrentQueryFromRequest(HttpRequest httpRequest) - { - var encoding = HttpHeader.GetEncodingFromContentType(httpRequest.Headers.ContentType); - var body = encoding.GetString(httpRequest.ContentData); - - var rpcBody = JsonConvert.DeserializeObject>(body); - - return JsonConvert.DeserializeObject(((JArray)rpcBody["params"])[1].ToJson()); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs index 4bc704ccd..e21852bf3 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; @@ -13,6 +12,7 @@ using NzbDrone.Core.Indexers.Definitions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests { @@ -22,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" } }; } @@ -38,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[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; - releases.Should().HaveCount(1462); + releases.Should().HaveCount(1464); releases.First().Should().BeOfType(); var torrentInfo = releases.First() as TorrentInfo; - torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM (2020) [Windows / Multi-Language / Full ISO]"); + torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM"); 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 06:39:11", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime()); torrentInfo.Size.Should().Be(80077617780); torrentInfo.InfoHash.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null); @@ -75,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[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 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 06326d162..debe6c891 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[] { 2000, 2010 }, + Categories = new int[] { 2000, 2010 }, ImdbId = "0076759" }; } @@ -52,12 +52,12 @@ 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; first.Guid.Should().Be("HDBits-257142"); - first.Title.Should().Be("Supernatural.S10E17.1080p.WEB-DL.DD5.1.H.264-ECI"); + first.Title.Should().Be("Supernatural S10E17 1080p WEB-DL DD5.1 H.264-ECI"); first.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); first.DownloadUrl.Should().Be("https://hdbits.org/download.php?id=257142&passkey=fakekey"); first.InfoUrl.Should().Be("https://hdbits.org/details.php?id=257142"); diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs index 06febb48b..cb4dacb88 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs @@ -6,7 +6,6 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.IndexerTests { @@ -32,15 +31,13 @@ namespace NzbDrone.Core.Test.IndexerTests Mocker.SetConstant(repo); var existingIndexers = Builder.CreateNew().BuildNew(); - existingIndexers.ConfigContract = nameof(NewznabSettings); + existingIndexers.ConfigContract = typeof(NewznabSettings).Name; repo.Insert(existingIndexers); Subject.Handle(new ApplicationStartedEvent()); AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id); - - ExceptionVerification.ExpectedWarns(1); } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs index f836852e1..55ed8abfb 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs @@ -68,16 +68,5 @@ namespace NzbDrone.Core.Test.IndexerTests VerifyNoUpdate(); } - - [Test] - public void should_not_record_failure_for_unknown_provider() - { - Subject.RecordFailure(0); - - Mocker.GetMock() - .Verify(v => v.FindByProviderId(1), Times.Never); - - VerifyNoUpdate(); - } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs index d63fdbe10..e643263f7 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabCapabilitiesProviderFixture.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net; +using System.Threading.Tasks; using System.Xml; using FluentAssertions; using Moq; diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs index 084767ffa..231c4bb56 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabFixture.cs @@ -33,10 +33,6 @@ 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/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index b26a1db21..ead7db168 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -19,11 +19,6 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests [SetUp] public void SetUp() { - Subject.Definition = new IndexerDefinition - { - Name = "Newznab" - }; - Subject.Settings = new NewznabSettings() { BaseUrl = "http://127.0.0.1:1234/", diff --git a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs index d33dde2d3..43ec59c53 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/OrpheusTests/OrpheusFixture.cs @@ -9,8 +9,8 @@ using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Definitions; +using NzbDrone.Core.Indexers.Definitions.Gazelle; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests @@ -39,12 +39,12 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases; - releases.Should().HaveCount(50); - releases.First().Should().BeOfType(); + releases.Should().HaveCount(65); + releases.First().Should().BeOfType(); - var torrentInfo = releases.First() as TorrentInfo; + var torrentInfo = releases.First() as GazelleInfo; - 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/PTPTests/PTPFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs index 1d6ad7271..c27972e0f 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/PTPTests/PTPFixture.cs @@ -6,8 +6,9 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Definitions.PassThePopcorn; +using NzbDrone.Core.Indexers.PassThePopcorn; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -20,22 +21,26 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests [SetUp] public void Setup() { - Subject.Definition = new IndexerDefinition + Subject.Definition = new IndexerDefinition() { Name = "PTP", - Settings = new PassThePopcornSettings - { - APIUser = "asdf", - APIKey = "sad" - } + Settings = new PassThePopcornSettings() { APIUser = "asdf", APIKey = "sad" } }; } [TestCase("Files/Indexers/PTP/imdbsearch.json")] public async Task should_parse_feed_from_PTP(string fileName) { + var authResponse = new PassThePopcornAuthResponse { Result = "Ok" }; + + System.IO.StringWriter authStream = new System.IO.StringWriter(); + Json.Serialize(authResponse, authStream); var responseJson = ReadAllText(fileName); + Mocker.GetMock() + .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Post), Subject.Definition)) + .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString()))); + Mocker.GetMock() .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson))); diff --git a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs index 02a205b27..f077357a2 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RarbgTests/RarbgFixture.cs @@ -17,7 +17,6 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.IndexerTests.RarbgTests { - [Obsolete("Rarbg has shutdown 2023-05-31")] [TestFixture] public class RarbgFixture : CoreTest { diff --git a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs index d7eb35cd1..447033938 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs @@ -9,8 +9,8 @@ using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Definitions; +using NzbDrone.Core.Indexers.Definitions.Gazelle; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.IndexerTests.RedactedTests @@ -40,14 +40,14 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases; releases.Should().HaveCount(39); - releases.First().Should().BeOfType(); + releases.First().Should().BeOfType(); - var torrentInfo = releases.First() as TorrentInfo; + var torrentInfo = releases.First() as GazelleInfo; - 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.sh/ajax.php?action=download&id=3892313"); - torrentInfo.InfoUrl.Should().Be("https://redacted.sh/torrents.php?id=16720&torrentid=3892313"); + torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313&usetoken=0"); + torrentInfo.InfoUrl.Should().Be("https://redacted.ch/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/SecretCinemaTests/SecretCinemaFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs index 7c5166d26..b62c5c7d1 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SecretCinemaTests/SecretCinemaFixture.cs @@ -11,7 +11,6 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Definitions; using NzbDrone.Core.Indexers.Definitions.Gazelle; using NzbDrone.Core.IndexerSearch.Definitions; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests @@ -22,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests [SetUp] public void Setup() { - Subject.Definition = new IndexerDefinition + Subject.Definition = new IndexerDefinition() { Name = "SecretCinema", Settings = new GazelleSettings { Username = "somekey", Password = "somekey" } @@ -41,13 +40,13 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; releases.Should().HaveCount(3); - releases.First().Should().BeOfType(); + releases.First().Should().BeOfType(); - var torrentInfo = releases.First() as TorrentInfo; + var torrentInfo = releases.First() as GazelleInfo; torrentInfo.Title.Should().Be("Singin' in the Rain (1952) 2160p"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&id=45068"); + torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&useToken=0&id=45068"); torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs index feed21e90..969f0c014 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs @@ -1,4 +1,5 @@ using NLog; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index a5fdf0506..e5a6f6c32 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -37,9 +37,6 @@ 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())) @@ -86,7 +83,23 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; - releases.Should().HaveCount(0); + 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); } [Test] diff --git a/src/NzbDrone.Core.Test/InstrumentationTests/DatabaseTargetFixture.cs b/src/NzbDrone.Core.Test/InstrumentationTests/DatabaseTargetFixture.cs index 738f4c2bc..705ebf767 100644 --- a/src/NzbDrone.Core.Test/InstrumentationTests/DatabaseTargetFixture.cs +++ b/src/NzbDrone.Core.Test/InstrumentationTests/DatabaseTargetFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.InstrumentationTests public void write_long_log() { var message = string.Empty; - for (var i = 0; i < 100; i++) + for (int i = 0; i < 100; i++) { message += Guid.NewGuid(); } diff --git a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs index cd1b189ed..e49b0592d 100644 --- a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Localization; using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Localization { @@ -28,20 +29,19 @@ namespace NzbDrone.Core.Test.Localization } [Test] - public void should_get_string_in_french() + public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists() { - Mocker.GetMock().Setup(m => m.UILanguage).Returns("fr"); + var localizedString = Subject.GetLocalizedString("BackupNow", "an"); - var localizedString = Subject.GetLocalizedString("BackupNow"); + localizedString.Should().Be("Backup Now"); - localizedString.Should().Be("Sauvegarder maintenant"); + ExceptionVerification.ExpectedErrors(1); } [Test] - public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists() + public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists() { - Mocker.GetMock().Setup(m => m.UILanguage).Returns(""); - var localizedString = Subject.GetLocalizedString("BackupNow"); + var localizedString = Subject.GetLocalizedString("BackupNow", ""); localizedString.Should().Be("Backup Now"); } @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.Localization [Test] public void should_return_argument_if_string_doesnt_exists() { - var localizedString = Subject.GetLocalizedString("BadString"); + var localizedString = Subject.GetLocalizedString("BadString", "en"); localizedString.Should().Be("BadString"); } diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs index e0f633266..7e22f6f85 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using Moq; using NUnit.Framework; @@ -210,6 +210,10 @@ namespace NzbDrone.Core.Test.Messaging.Commands public class CommandB : Command { + public CommandB() + { + } + public override string CompletionMessage => null; } } diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs index 87e49e52f..1de87a768 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueFixture.cs @@ -1,4 +1,5 @@ using FizzWare.NBuilder; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs index c69c911b7..487c40b2a 100644 --- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs +++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.Messaging.Commands; diff --git a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs deleted file mode 100644 index 309169c69..000000000 --- a/src/NzbDrone.Core.Test/NotificationTests/EmailTests/EmailSettingsValidatorFixture.cs +++ /dev/null @@ -1,111 +0,0 @@ -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/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs index 42c92c909..366687860 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs @@ -51,11 +51,6 @@ namespace NzbDrone.Core.Test.NotificationTests TestLogger.Info("OnHealthIssue was called"); } - public override void OnHealthRestored(Core.HealthCheck.HealthCheck healthCheck) - { - TestLogger.Info("OnHealthRestored was called"); - } - public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { TestLogger.Info("OnApplicationUpdate was called"); @@ -84,7 +79,6 @@ namespace NzbDrone.Core.Test.NotificationTests var notification = new TestNotificationWithAllEvents(); notification.SupportsOnHealthIssue.Should().BeTrue(); - notification.SupportsOnHealthRestored.Should().BeTrue(); notification.SupportsOnApplicationUpdate.Should().BeTrue(); notification.SupportsOnGrab.Should().BeTrue(); } @@ -95,7 +89,6 @@ namespace NzbDrone.Core.Test.NotificationTests var notification = new TestNotificationWithNoEvents(); notification.SupportsOnHealthIssue.Should().BeFalse(); - notification.SupportsOnHealthRestored.Should().BeFalse(); notification.SupportsOnApplicationUpdate.Should().BeFalse(); notification.SupportsOnGrab.Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs deleted file mode 100644 index 183246313..000000000 --- a/src/NzbDrone.Core.Test/NotificationTests/NotificationStatusServiceFixture.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Notifications; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.NotificationTests -{ - public class NotificationStatusServiceFixture : CoreTest - { - private DateTime _epoch; - - [SetUp] - public void SetUp() - { - _epoch = DateTime.UtcNow; - - Mocker.GetMock() - .SetupGet(v => v.StartTime) - .Returns(_epoch - TimeSpan.FromHours(1)); - } - - private NotificationStatus WithStatus(NotificationStatus status) - { - Mocker.GetMock() - .Setup(v => v.FindByProviderId(1)) - .Returns(status); - - Mocker.GetMock() - .Setup(v => v.All()) - .Returns(new[] { status }); - - return status; - } - - private void VerifyUpdate() - { - Mocker.GetMock() - .Verify(v => v.Upsert(It.IsAny()), Times.Once()); - } - - private void VerifyNoUpdate() - { - Mocker.GetMock() - .Verify(v => v.Upsert(It.IsAny()), Times.Never()); - } - - [Test] - public void should_not_consider_blocked_within_5_minutes_since_initial_failure() - { - WithStatus(new NotificationStatus - { - InitialFailure = _epoch - TimeSpan.FromMinutes(4), - MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), - EscalationLevel = 3 - }); - - Subject.RecordFailure(1); - - VerifyUpdate(); - - var status = Subject.GetBlockedProviders().FirstOrDefault(); - status.Should().BeNull(); - } - - [Test] - public void should_consider_blocked_after_5_minutes_since_initial_failure() - { - WithStatus(new NotificationStatus - { - InitialFailure = _epoch - TimeSpan.FromMinutes(6), - MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), - EscalationLevel = 3 - }); - - Subject.RecordFailure(1); - - VerifyUpdate(); - - var status = Subject.GetBlockedProviders().FirstOrDefault(); - status.Should().NotBeNull(); - } - - [Test] - public void should_not_escalate_further_till_after_5_minutes_since_initial_failure() - { - var origStatus = WithStatus(new NotificationStatus - { - InitialFailure = _epoch - TimeSpan.FromMinutes(4), - MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), - EscalationLevel = 3 - }); - - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - - var status = Subject.GetBlockedProviders().FirstOrDefault(); - status.Should().BeNull(); - - origStatus.EscalationLevel.Should().Be(3); - } - - [Test] - public void should_escalate_further_after_5_minutes_since_initial_failure() - { - WithStatus(new NotificationStatus - { - InitialFailure = _epoch - TimeSpan.FromMinutes(6), - MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), - EscalationLevel = 3 - }); - - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - - var status = Subject.GetBlockedProviders().FirstOrDefault(); - status.Should().NotBeNull(); - - status.EscalationLevel.Should().BeGreaterThan(3); - } - - [Test] - public void should_not_escalate_beyond_3_hours() - { - WithStatus(new NotificationStatus - { - InitialFailure = _epoch - TimeSpan.FromMinutes(6), - MostRecentFailure = _epoch - TimeSpan.FromSeconds(120), - EscalationLevel = 3 - }); - - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - Subject.RecordFailure(1); - - var status = Subject.GetBlockedProviders().FirstOrDefault(); - status.Should().NotBeNull(); - status.DisabledTill.Should().HaveValue(); - status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1)); - } - } -} diff --git a/src/NzbDrone.Core.Test/ParserTests/ParseUtilFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParseUtilFixture.cs index 85ea8bf52..badfe7c92 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParseUtilFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParseUtilFixture.cs @@ -63,22 +63,5 @@ namespace NzbDrone.Core.Test.ParserTests { ParseUtil.GetLongFromString(original).Should().Be(parsedInt); } - - [TestCase("tt0183790", "tt0183790")] - [TestCase("0183790", "tt0183790")] - [TestCase("183790", "tt0183790")] - [TestCase("tt10001870", "tt10001870")] - [TestCase("10001870", "tt10001870")] - [TestCase("tt", null)] - [TestCase("tt0", null)] - [TestCase("abc", null)] - [TestCase("abc0", null)] - [TestCase("0", null)] - [TestCase("", null)] - [TestCase(null, null)] - public void should_parse_full_imdb_id_from_string(string input, string expected) - { - ParseUtil.GetFullImdbId(input).Should().Be(expected); - } } } diff --git a/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Prowlarr.Core.Test.csproj index 13bc15cbc..3609cb943 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/ThingiProviderTests/ProviderStatusServiceFixture.cs b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs index dfe58dbb0..8e2f6b8c6 100644 --- a/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ThingiProviderTests/ProviderStatusServiceFixture.cs @@ -34,7 +34,6 @@ namespace NzbDrone.Core.Test.ThingiProviderTests public class ProviderStatusServiceFixture : CoreTest { - private readonly TimeSpan _disabledTillPrecision = TimeSpan.FromMilliseconds(500); private DateTime _epoch; [SetUp] @@ -91,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests var status = Subject.GetBlockedProviders().FirstOrDefault(); status.Should().NotBeNull(); status.DisabledTill.Should().HaveValue(); - status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), _disabledTillPrecision); + status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500); } [Test] @@ -134,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests var status = Subject.GetBlockedProviders().FirstOrDefault(); status.Should().NotBeNull(); status.DisabledTill.Should().HaveValue(); - status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), _disabledTillPrecision); + status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); } [Test] @@ -161,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests status.Should().NotBeNull(); origStatus.EscalationLevel.Should().Be(3); - status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), _disabledTillPrecision); + status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500); } } } diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index 5f2aa2662..ce2c446e6 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.1.0.0.0.tar.gz", + FileName = "NzbDrone.develop.2.0.0.0.tar.gz", Url = "http://download.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz", - Version = new Version("1.0.0.0") + Version = new Version("2.0.0.0") }; } else { _updatePackage = new UpdatePackage { - FileName = "NzbDrone.develop.1.0.0.0.zip", + FileName = "NzbDrone.develop.2.0.0.0.zip", Url = "http://download.sonarr.tv/v2/develop/windows/NzbDrone.develop.zip", - Version = new Version("1.0.0.0") + Version = new Version("2.0.0.0") }; } @@ -90,6 +90,17 @@ 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() { @@ -327,28 +338,6 @@ 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 8d31502fa..bb56e1a02 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -15,7 +15,6 @@ namespace NzbDrone.Core.Annotations public string Label { get; set; } public string Unit { get; set; } public string HelpText { get; set; } - public string HelpTextWarning { get; set; } public string HelpLink { get; set; } public FieldType Type { get; set; } public bool Advanced { get; set; } @@ -41,23 +40,6 @@ 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; } @@ -99,11 +81,4 @@ 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 349e50622..ff966b196 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 AppIndexerMapRepository : BasicRepository, IAppIndexerMapRepository + public class TagRepository : BasicRepository, IAppIndexerMapRepository { - public AppIndexerMapRepository(IMainDatabase database, IEventAggregator eventAggregator) + public TagRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { } diff --git a/src/NzbDrone.Core/Applications/ApplicationBase.cs b/src/NzbDrone.Core/Applications/ApplicationBase.cs index b4d32f054..82fb56d86 100644 --- a/src/NzbDrone.Core/Applications/ApplicationBase.cs +++ b/src/NzbDrone.Core/Applications/ApplicationBase.cs @@ -11,8 +11,6 @@ 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; @@ -29,10 +27,9 @@ namespace NzbDrone.Core.Applications protected TSettings Settings => (TSettings)Definition.Settings; - public ApplicationBase(IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public ApplicationBase(IAppIndexerMapService appIndexerMapService, Logger logger) { _appIndexerMapService = appIndexerMapService; - _indexerFactory = indexerFactory; _logger = logger; } @@ -49,7 +46,8 @@ namespace NzbDrone.Core.Applications yield return new ApplicationDefinition { - SyncLevel = ApplicationSyncLevel.FullSync, + Name = GetType().Name, + SyncLevel = ApplicationSyncLevel.AddOnly, Implementation = GetType().Name, Settings = config }; @@ -57,7 +55,7 @@ namespace NzbDrone.Core.Applications } public abstract void AddIndexer(IndexerDefinition indexer); - public abstract void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false); + public abstract void UpdateIndexer(IndexerDefinition indexer); public abstract void RemoveIndexer(int indexerId); public abstract List GetIndexerMappings(); @@ -65,17 +63,5 @@ 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/ApplicationDefinition.cs b/src/NzbDrone.Core/Applications/ApplicationDefinition.cs index ed01daffc..3160898f1 100644 --- a/src/NzbDrone.Core/Applications/ApplicationDefinition.cs +++ b/src/NzbDrone.Core/Applications/ApplicationDefinition.cs @@ -6,6 +6,6 @@ namespace NzbDrone.Core.Applications { public ApplicationSyncLevel SyncLevel { get; set; } - public override bool Enable => SyncLevel is ApplicationSyncLevel.AddOnly or ApplicationSyncLevel.FullSync; + public override bool Enable => SyncLevel == ApplicationSyncLevel.AddOnly || SyncLevel == ApplicationSyncLevel.FullSync; } } diff --git a/src/NzbDrone.Core/Applications/ApplicationFactory.cs b/src/NzbDrone.Core/Applications/ApplicationFactory.cs index 2ecd2e78b..1b8dc1e2e 100644 --- a/src/NzbDrone.Core/Applications/ApplicationFactory.cs +++ b/src/NzbDrone.Core/Applications/ApplicationFactory.cs @@ -42,18 +42,14 @@ namespace NzbDrone.Core.Applications return enabledClients.ToList(); } - protected override List Active() - { - return base.Active().Where(c => c.Enable).ToList(); - } - private IEnumerable FilterBlockedApplications(IEnumerable applications) { var blockedApplications = _applicationStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); foreach (var application in applications) { - if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus) && blockedApplicationStatus.DisabledTill.HasValue) + ApplicationStatus blockedApplicationStatus; + if (blockedApplications.TryGetValue(application.Definition.Id, out blockedApplicationStatus)) { _logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime()); continue; @@ -67,19 +63,10 @@ namespace NzbDrone.Core.Applications { var result = base.Test(definition); - if (definition.Id == 0) - { - return result; - } - - if (result == null || result.IsValid) + if ((result == null || result.IsValid) && definition.Id != 0) { _applicationStatusService.RecordSuccess(definition.Id); } - else - { - _applicationStatusService.RecordFailure(definition.Id); - } return result; } diff --git a/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs b/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs index 769843dd3..ad9023993 100644 --- a/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs +++ b/src/NzbDrone.Core/Applications/ApplicationIndexerSyncCommand.cs @@ -4,15 +4,8 @@ namespace NzbDrone.Core.Applications { public class ApplicationIndexerSyncCommand : Command { - public bool ForceSync { get; set; } - - public ApplicationIndexerSyncCommand() - { - ForceSync = false; - } - public override bool SendUpdatesToClient => true; - public override string CompletionMessage => "Completed"; + public override string CompletionMessage => null; } } diff --git a/src/NzbDrone.Core/Applications/ApplicationService.cs b/src/NzbDrone.Core/Applications/ApplicationService.cs index 0a5a81fbd..512c2a9bb 100644 --- a/src/NzbDrone.Core/Applications/ApplicationService.cs +++ b/src/NzbDrone.Core/Applications/ApplicationService.cs @@ -67,11 +67,10 @@ namespace NzbDrone.Core.Applications public void HandleAsync(ProviderAddedEvent message) { var enabledApps = _applicationsFactory.SyncEnabled(); - var indexer = _indexerFactory.GetInstance((IndexerDefinition)message.Definition); foreach (var app in enabledApps) { - if (ShouldHandleIndexer(app.Definition, indexer.Definition)) + if (ShouldHandleIndexer(app.Definition, message.Definition)) { ExecuteAction(a => a.AddIndexer((IndexerDefinition)message.Definition), app); } @@ -93,9 +92,8 @@ namespace NzbDrone.Core.Applications var enabledApps = _applicationsFactory.SyncEnabled() .Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync) .ToList(); - var indexer = _indexerFactory.GetInstance((IndexerDefinition)message.Definition); - SyncIndexers(enabledApps, new List { (IndexerDefinition)indexer.Definition }); + SyncIndexers(enabledApps, new List { (IndexerDefinition)message.Definition }); } public void HandleAsync(ApiKeyChangedEvent message) @@ -104,7 +102,7 @@ namespace NzbDrone.Core.Applications var indexers = _indexerFactory.AllProviders().Select(i => (IndexerDefinition)i.Definition).ToList(); - SyncIndexers(enabledApps, indexers, true, true); + SyncIndexers(enabledApps, indexers, true); } public void HandleAsync(ProviderBulkUpdatedEvent message) @@ -122,13 +120,11 @@ namespace NzbDrone.Core.Applications var indexers = _indexerFactory.AllProviders().Select(i => (IndexerDefinition)i.Definition).ToList(); - SyncIndexers(enabledApps, indexers, true, message.ForceSync); + SyncIndexers(enabledApps, indexers, true); } - private void SyncIndexers(List applications, List indexers, bool removeRemote = false, bool forceSync = false) + private void SyncIndexers(List applications, List indexers, bool removeRemote = false) { - var sortedIndexers = indexers.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList(); - foreach (var app in applications) { var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id); @@ -159,7 +155,7 @@ namespace NzbDrone.Core.Applications } } - foreach (var indexer in sortedIndexers) + foreach (var indexer in indexers) { var definition = indexer; @@ -167,7 +163,7 @@ namespace NzbDrone.Core.Applications { if (((ApplicationDefinition)app.Definition).SyncLevel == ApplicationSyncLevel.FullSync && ShouldHandleIndexer(app.Definition, indexer)) { - ExecuteAction(a => a.UpdateIndexer(definition, forceSync), app); + ExecuteAction(a => a.UpdateIndexer(definition), app); } } else @@ -204,17 +200,9 @@ namespace NzbDrone.Core.Applications private bool ShouldHandleIndexer(ProviderDefinition app, ProviderDefinition indexer) { - if (!indexer.Settings.Validate().IsValid) - { - _logger.Debug("Indexer {0} [{1}] has invalid settings.", indexer.Name, indexer.Id); - - return false; - } - if (app.Tags.Empty()) { _logger.Debug("No tags set to application {0}.", app.Name); - return true; } @@ -222,13 +210,11 @@ namespace NzbDrone.Core.Applications if (intersectingTags.Any()) { - _logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting (matching) tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length); - + _logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length); return true; } - _logger.Debug("Application {0} does not have any intersecting (matching) tags with {1} [{2}]. Indexer will neither be synced to nor removed from the application.", app.Name, indexer.Name, indexer.Id); - + _logger.Info("Application {0} does not have any intersecting tags with {1} [{2}]. Indexer will not be handled.", app.Name, indexer.Name, indexer.Id); return false; } @@ -254,11 +240,11 @@ namespace NzbDrone.Core.Applications if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) { - _logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message); + _logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message); } else { - _logger.Warn(webException, "{0} {1}", this, webException.Message); + _logger.Warn("{0} {1}", this, webException.Message); } } catch (TooManyRequestsException ex) @@ -266,12 +252,12 @@ namespace NzbDrone.Core.Applications var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1); _applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff); - _logger.Warn(ex, "API Request Limit reached for {0}", this); + _logger.Warn("API Request Limit reached for {0}", this); } catch (HttpException ex) { _applicationStatusService.RecordFailure(application.Definition.Id); - _logger.Warn(ex, "{0} {1}", this, ex.Message); + _logger.Warn("{0} {1}", this, ex.Message); } catch (Exception ex) { @@ -303,11 +289,11 @@ namespace NzbDrone.Core.Applications if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) { - _logger.Warn(webException, "{0} server is currently unavailable. {1}", this, webException.Message); + _logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message); } else { - _logger.Warn(webException, "{0} {1}", this, webException.Message); + _logger.Warn("{0} {1}", this, webException.Message); } } catch (TooManyRequestsException ex) @@ -315,12 +301,12 @@ namespace NzbDrone.Core.Applications var minimumBackOff = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1); _applicationStatusService.RecordFailure(application.Definition.Id, minimumBackOff); - _logger.Warn(ex, "API Request Limit reached for {0}", this); + _logger.Warn("API Request Limit reached for {0}", this); } catch (HttpException ex) { _applicationStatusService.RecordFailure(application.Definition.Id); - _logger.Warn(ex, "{0} {1}", this, ex.Message); + _logger.Warn("{0} {1}", this, ex.Message); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Applications/IApplication.cs b/src/NzbDrone.Core/Applications/IApplication.cs index 599010ee1..5dd4572f5 100644 --- a/src/NzbDrone.Core/Applications/IApplication.cs +++ b/src/NzbDrone.Core/Applications/IApplication.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Applications public interface IApplication : IProvider { void AddIndexer(IndexerDefinition indexer); - void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false); + void UpdateIndexer(IndexerDefinition indexer); void RemoveIndexer(int indexerId); List GetIndexerMappings(); } diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs index 108972d6e..e965d12b4 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -16,8 +17,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _lazyLibrarianV1Proxy = lazyLibrarianV1Proxy; _configFileProvider = configFileProvider; @@ -31,10 +32,10 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { failures.AddIfNotNull(_lazyLibrarianV1Proxy.TestConnection(Settings)); } - catch (Exception ex) + catch (WebException ex) { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to LazyLibrarian. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to LazyLibrarian")); } return new ValidationResult(failures); @@ -65,9 +66,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -76,17 +75,9 @@ namespace NzbDrone.Core.Applications.LazyLibrarian _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexerCapabilities, indexer.Protocol); + var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol); var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" }); } @@ -105,28 +96,25 @@ namespace NzbDrone.Core.Applications.LazyLibrarian } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerProps[1]); + var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, 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); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} found", remoteIndexer.Name); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!lazyLibrarianIndexer.Equals(remoteIndexer) || forceSync) + if (!lazyLibrarianIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - _lazyLibrarianV1Proxy.UpdateIndexer(lazyLibrarianIndexer, Settings); indexerMapping.RemoteIndexerName = $"{lazyLibrarianIndexer.Type},{lazyLibrarianIndexer.Altername}"; _appIndexerMapService.Update(indexerMapping); @@ -136,7 +124,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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); @@ -149,7 +137,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian } } - private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null) + private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null) { var schema = protocol == DownloadProtocol.Usenet ? LazyLibrarianProviderType.Newznab : LazyLibrarianProviderType.Torznab; @@ -159,19 +147,12 @@ namespace NzbDrone.Core.Applications.LazyLibrarian Altername = $"{indexer.Name} (Prowlarr)", Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, - Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), + Categories = string.Join(",", indexer.Capabilities.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 b13403aac..bc20c3ddf 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianIndexer.cs @@ -31,9 +31,6 @@ 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) { @@ -48,10 +45,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian other.Categories == Categories && other.Enabled == Enabled && other.Altername == Altername && - other.Priority == Priority && - other.SeedRatio == SeedRatio && - other.SeedTime == SeedTime && - other.MinimumSeeders == MinimumSeeders; + other.Priority == Priority; } } } diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianSettings.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianSettings.cs index 9b2d17221..ab5590c15 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianSettings.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianSettings.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian [FieldDefinition(1, Label = "LazyLibrarian Server", HelpText = "URL used to connect to LazyLibrarian server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:5299")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by LazyLibrarian in Settings/Web Interface")] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by LazyLibrarian in Settings/Web Interface")] 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")] diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs index f4c6da138..43df7764f 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using FluentValidation.Results; +using Newtonsoft.Json; using NLog; using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Applications.LazyLibrarian { @@ -97,13 +96,6 @@ 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; @@ -123,13 +115,6 @@ 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; @@ -154,11 +139,11 @@ namespace NzbDrone.Core.Applications.LazyLibrarian return new ValidationFailure("ApiKey", status.Error.Message); } - GetIndexers(settings); + var indexers = GetIndexers(settings); } catch (HttpException ex) { - _logger.Error(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to send test message"); return new ValidationFailure("BaseUrl", "Unable to complete application test"); } catch (LazyLibrarianException ex) @@ -168,8 +153,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian } catch (Exception ex) { - _logger.Error(ex, "Unable to complete application test"); - return new ValidationFailure("", $"Unable to send test message. {ex.Message}"); + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); } return null; @@ -179,9 +164,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var requestBuilder = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var requestBuilder = new HttpRequestBuilder(baseUrl).Resource(resource) .AddQueryParam("cmd", command) .AddQueryParam("apikey", settings.ApiKey); @@ -208,12 +191,9 @@ namespace NzbDrone.Core.Applications.LazyLibrarian { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } private int CalculatePriority(int indexerPriority) => ProwlarrHighestPriority - indexerPriority + 1; diff --git a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs index 19c842f5c..30965e6ca 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; using FluentValidation.Results; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; @@ -22,8 +20,8 @@ namespace NzbDrone.Core.Applications.Lidarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _lidarrV1Proxy = lidarrV1Proxy; @@ -49,40 +47,12 @@ namespace NzbDrone.Core.Applications.Lidarr try { - failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); } - catch (HttpException ex) + catch (WebException ex) { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); - failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid")); - break; - case HttpStatusCode.BadRequest: - _logger.Warn(ex, "Prowlarr URL is invalid"); - 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; - default: - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Lidarr. {ex.Message}")); - break; - } - } - catch (JsonReaderException ex) - { - _logger.Error(ex, "Unable to parse JSON response from application"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}")); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Lidarr. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Lidarr")); } return new ValidationResult(failures); @@ -91,26 +61,21 @@ namespace NzbDrone.Core.Applications.Lidarr public override List GetIndexerMappings() { var indexers = _lidarrV1Proxy.GetIndexers(Settings) - .Where(i => i.Implementation is "Newznab" or "Torznab"); + .Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; - - if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey) { - continue; - } + var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value); - var match = AppIndexerRegex.Match(baseUrl); - - if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) - { - // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance - mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) + { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance + mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + } } } @@ -119,9 +84,7 @@ namespace NzbDrone.Core.Applications.Lidarr public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -130,17 +93,9 @@ namespace NzbDrone.Core.Applications.Lidarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var lidarrIndexer = BuildLidarrIndexer(indexer, indexerCapabilities, indexer.Protocol); + var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol); var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); } @@ -158,27 +113,24 @@ namespace NzbDrone.Core.Applications.Lidarr } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _lidarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!lidarrIndexer.Equals(remoteIndexer) || forceSync) + if (!lidarrIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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))); @@ -204,7 +156,7 @@ namespace NzbDrone.Core.Applications.Lidarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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; @@ -218,17 +170,11 @@ namespace NzbDrone.Core.Applications.Lidarr } } - private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) + private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, 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", "rejectBlocklistedTorrentHashesWhileGrabbing" }; - - if (id == 0) - { - // Ensuring backward compatibility with older versions on first sync - syncFields.AddRange(new List { "earlyReleaseLimit", "additionalParameters" }); - } + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -254,7 +200,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(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { @@ -262,15 +208,10 @@ 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.Any(x => x.Name == "seedCriteria.discographySeedTime")) + if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) { 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/LidarrField.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrField.cs index 9637e48d1..bfbf59dea 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrField.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrField.cs @@ -2,7 +2,12 @@ namespace NzbDrone.Core.Applications.Lidarr { public class LidarrField { + public int Order { get; set; } public string Name { get; set; } + public string Label { get; set; } + public string Unit { get; set; } + public string HelpText { get; set; } + public string HelpLink { get; set; } public object Value { get; set; } public string Type { get; set; } public bool Advanced { get; set; } diff --git a/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs index 98c6125ff..7ac02fd45 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrIndexer.cs @@ -29,12 +29,9 @@ namespace NzbDrone.Core.Applications.Lidarr } var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value; var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); - var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPathCompare = apiPath.Equals(otherApiPath); @@ -55,10 +52,6 @@ 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 && @@ -66,7 +59,7 @@ namespace NzbDrone.Core.Applications.Lidarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; + apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs b/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs index 0197255a2..a39cc7637 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs @@ -33,15 +33,12 @@ namespace NzbDrone.Core.Applications.Lidarr [FieldDefinition(1, Label = "Lidarr Server", HelpText = "URL used to connect to Lidarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8686")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", 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), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] 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 3fcb337c0..0ddb25e55 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs @@ -23,11 +23,8 @@ namespace NzbDrone.Core.Applications.Lidarr public class LidarrV1Proxy : ILidarrV1Proxy { - private static Version MinimumApplicationVersion => new (1, 0, 2, 0); - private const string AppApiRoute = "/api/v1"; private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer"; - private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -84,20 +81,8 @@ namespace NzbDrone.Core.Applications.Lidarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to add indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings) @@ -105,20 +90,8 @@ namespace NzbDrone.Core.Applications.Lidarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to update indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings) @@ -126,18 +99,38 @@ namespace NzbDrone.Core.Applications.Lidarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); - - if (applicationVersion == null) + try { - return new ValidationFailure(string.Empty, "Failed to fetch Lidarr version"); + Execute(request); } - - if (new Version(applicationVersion) < MinimumApplicationVersion) + catch (HttpException ex) { - return new ValidationFailure(string.Empty, $"Lidarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "API Key is invalid"); + return new ValidationFailure("ApiKey", "API Key is invalid"); + } + + if (ex.Response.StatusCode == HttpStatusCode.BadRequest) + { + _logger.Error(ex, "Prowlarr URL is invalid"); + return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr"); + } + + if (ex.Response.StatusCode == HttpStatusCode.SeeOther) + { + _logger.Error(ex, "Lidarr returned redirect and is invalid"); + return new ValidationFailure("BaseUrl", "Lidarr url is invalid, Prowlarr cannot connect to Lidarr - are you missing a url base?"); + } + + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("BaseUrl", "Unable to complete application test"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); } return null; @@ -154,30 +147,27 @@ namespace NzbDrone.Core.Applications.Lidarr switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); + _logger.Error(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); + _logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); + _logger.Error(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found"); + _logger.Error(ex, "Remote indexer not found"); break; default: _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); - break; + throw; } - - throw; } catch (JsonReaderException ex) { @@ -189,15 +179,15 @@ namespace NzbDrone.Core.Applications.Lidarr _logger.Error(ex, "Unable to add or update indexer"); throw; } + + return null; } private HttpRequest BuildRequest(LidarrSettings settings, string resource, HttpMethod method) { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var request = new HttpRequestBuilder(baseUrl).Resource(resource) .SetHeader("X-Api-Key", settings.ApiKey) .Build(); @@ -214,12 +204,9 @@ namespace NzbDrone.Core.Applications.Lidarr { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs index e9fd9ffe7..052a96ff1 100644 --- a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs +++ b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -16,8 +17,8 @@ namespace NzbDrone.Core.Applications.Mylar private readonly IMylarV3Proxy _mylarV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _mylarV3Proxy = mylarV3Proxy; _configFileProvider = configFileProvider; @@ -31,10 +32,10 @@ namespace NzbDrone.Core.Applications.Mylar { failures.AddIfNotNull(_mylarV3Proxy.TestConnection(Settings)); } - catch (Exception ex) + catch (WebException ex) { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Mylar. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Mylar")); } return new ValidationResult(failures); @@ -65,9 +66,7 @@ namespace NzbDrone.Core.Applications.Mylar public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -76,17 +75,9 @@ namespace NzbDrone.Core.Applications.Mylar _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var mylarIndexer = BuildMylarIndexer(indexer, indexerCapabilities, indexer.Protocol); + var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol); var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" }); } @@ -105,28 +96,25 @@ namespace NzbDrone.Core.Applications.Mylar } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerProps[1]); + var mylarIndexer = BuildMylarIndexer(indexer, 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); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} found", remoteIndexer.Name); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!mylarIndexer.Equals(remoteIndexer) || forceSync) + if (!mylarIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - _mylarV3Proxy.UpdateIndexer(mylarIndexer, Settings); indexerMapping.RemoteIndexerName = $"{mylarIndexer.Type},{mylarIndexer.Altername}"; _appIndexerMapService.Update(indexerMapping); @@ -136,7 +124,7 @@ namespace NzbDrone.Core.Applications.Mylar { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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); @@ -149,7 +137,7 @@ namespace NzbDrone.Core.Applications.Mylar } } - private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, string originalName = null) + private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null) { var schema = protocol == DownloadProtocol.Usenet ? MylarProviderType.Newznab : MylarProviderType.Torznab; @@ -159,7 +147,7 @@ namespace NzbDrone.Core.Applications.Mylar Altername = $"{indexer.Name} (Prowlarr)", Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, - Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), + Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), Enabled = indexer.Enable, Type = schema, }; diff --git a/src/NzbDrone.Core/Applications/Mylar/MylarSettings.cs b/src/NzbDrone.Core/Applications/Mylar/MylarSettings.cs index f6f58b9e5..385be12e1 100644 --- a/src/NzbDrone.Core/Applications/Mylar/MylarSettings.cs +++ b/src/NzbDrone.Core/Applications/Mylar/MylarSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Applications.Mylar [FieldDefinition(1, Label = "Mylar Server", HelpText = "URL used to connect to Mylar server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8090")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Mylar in Settings/Web Interface")] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Mylar in Settings/Web Interface")] 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")] diff --git a/src/NzbDrone.Core/Applications/Mylar/MylarV3Proxy.cs b/src/NzbDrone.Core/Applications/Mylar/MylarV3Proxy.cs index 119175634..4c72160e9 100644 --- a/src/NzbDrone.Core/Applications/Mylar/MylarV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Mylar/MylarV3Proxy.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using FluentValidation.Results; +using Newtonsoft.Json; using NLog; using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Applications.Mylar { @@ -135,11 +135,11 @@ namespace NzbDrone.Core.Applications.Mylar return new ValidationFailure("ApiKey", status.Error.Message); } - GetIndexers(settings); + var indexers = GetIndexers(settings); } catch (HttpException ex) { - _logger.Error(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to send test message"); return new ValidationFailure("BaseUrl", "Unable to complete application test"); } catch (MylarException ex) @@ -149,8 +149,8 @@ namespace NzbDrone.Core.Applications.Mylar } catch (Exception ex) { - _logger.Error(ex, "Unable to complete application test"); - return new ValidationFailure("", $"Unable to send test message. {ex.Message}"); + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); } return null; @@ -160,9 +160,7 @@ namespace NzbDrone.Core.Applications.Mylar { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var requestBuilder = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var requestBuilder = new HttpRequestBuilder(baseUrl).Resource(resource) .AddQueryParam("cmd", command) .AddQueryParam("apikey", settings.ApiKey); @@ -189,12 +187,9 @@ namespace NzbDrone.Core.Applications.Mylar { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs index 85b9c4a3b..41e0799ee 100644 --- a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs +++ b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; using FluentValidation.Results; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; @@ -22,8 +20,8 @@ namespace NzbDrone.Core.Applications.Radarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _radarrV3Proxy = radarrV3Proxy; @@ -49,40 +47,12 @@ namespace NzbDrone.Core.Applications.Radarr try { - failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); } - catch (HttpException ex) + catch (WebException ex) { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); - failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid")); - break; - case HttpStatusCode.BadRequest: - _logger.Warn(ex, "Prowlarr URL is invalid"); - 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; - default: - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Radarr. {ex.Message}")); - break; - } - } - catch (JsonReaderException ex) - { - _logger.Error(ex, "Unable to parse JSON response from application"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}")); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Radarr. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Radarr")); } return new ValidationResult(failures); @@ -91,26 +61,21 @@ namespace NzbDrone.Core.Applications.Radarr public override List GetIndexerMappings() { var indexers = _radarrV3Proxy.GetIndexers(Settings) - .Where(i => i.Implementation is "Newznab" or "Torznab"); + .Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; - - if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey) { - continue; - } + var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value); - var match = AppIndexerRegex.Match(baseUrl); - - if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) - { - // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance - mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) + { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance + mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + } } } @@ -119,9 +84,7 @@ namespace NzbDrone.Core.Applications.Radarr public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -130,17 +93,9 @@ namespace NzbDrone.Core.Applications.Radarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var radarrIndexer = BuildRadarrIndexer(indexer, indexerCapabilities, indexer.Protocol); + var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol); var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); } @@ -158,25 +113,24 @@ namespace NzbDrone.Core.Applications.Radarr } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _radarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!radarrIndexer.Equals(remoteIndexer) || forceSync) + if (!radarrIndexer.Equals(remoteIndexer)) { - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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))); @@ -202,7 +156,7 @@ namespace NzbDrone.Core.Applications.Radarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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; @@ -216,17 +170,11 @@ namespace NzbDrone.Core.Applications.Radarr } } - private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) + private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, 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", "rejectBlocklistedTorrentHashesWhileGrabbing" }; - - if (id == 0) - { - // Ensuring backward compatibility with older versions on first sync - syncFields.AddRange(new List { "multiLanguages", "removeYear", "requiredFlags", "additionalParameters" }); - } + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -252,18 +200,13 @@ 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(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + radarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.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/RadarrField.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs index fbafc3b9c..322b97116 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs @@ -2,7 +2,12 @@ namespace NzbDrone.Core.Applications.Radarr { public class RadarrField { + public int Order { get; set; } public string Name { get; set; } + public string Label { get; set; } + public string Unit { get; set; } + public string HelpText { get; set; } + public string HelpLink { get; set; } public object Value { get; set; } public string Type { get; set; } public bool Advanced { get; set; } diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs index 3ae820f3a..38082724e 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs @@ -29,12 +29,9 @@ namespace NzbDrone.Core.Applications.Radarr } var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value; var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); - var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPathCompare = apiPath.Equals(otherApiPath); @@ -51,10 +48,6 @@ 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 && @@ -62,7 +55,7 @@ namespace NzbDrone.Core.Applications.Radarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; + apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs index 457d7d0df..68a98879c 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Applications.Radarr { ProwlarrUrl = "http://localhost:9696"; BaseUrl = "http://localhost:7878"; - SyncCategories = new[] { 2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090 }; + SyncCategories = new[] { 2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080 }; } [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")] @@ -34,15 +34,12 @@ namespace NzbDrone.Core.Applications.Radarr [FieldDefinition(1, Label = "Radarr Server", HelpText = "URL used to connect to Radarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:7878")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", 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), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] 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 d431856aa..6e4efa1a1 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs @@ -23,12 +23,8 @@ namespace NzbDrone.Core.Applications.Radarr public class RadarrV3Proxy : IRadarrV3Proxy { - private static Version MinimumApplicationV4Version => new (4, 0, 4, 0); - private static Version MinimumApplicationV3Version => new (3, 1, 1, 0); - private const string AppApiRoute = "/api/v3"; private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer"; - private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -85,20 +81,8 @@ namespace NzbDrone.Core.Applications.Radarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to add indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings) @@ -106,20 +90,8 @@ namespace NzbDrone.Core.Applications.Radarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to update indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings) @@ -127,30 +99,38 @@ namespace NzbDrone.Core.Applications.Radarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); - - if (applicationVersion == null) + try { - return new ValidationFailure(string.Empty, "Failed to fetch Radarr version"); + Execute(request); } - - var version = new Version(applicationVersion); - - if (version.Major == 3) + catch (HttpException ex) { - if (version < MinimumApplicationV3Version) + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) { - return new ValidationFailure(string.Empty, $"Radarr version should be at least {MinimumApplicationV3Version.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); + _logger.Error(ex, "API Key is invalid"); + return new ValidationFailure("ApiKey", "API Key is invalid"); } + + if (ex.Response.StatusCode == HttpStatusCode.BadRequest) + { + _logger.Error(ex, "Prowlarr URL is invalid"); + return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr"); + } + + if (ex.Response.StatusCode == HttpStatusCode.SeeOther) + { + _logger.Error(ex, "Radarr returned redirect and is invalid"); + return new ValidationFailure("BaseUrl", "Radarr url is invalid, Prowlarr cannot connect to Radarr - are you missing a url base?"); + } + + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("BaseUrl", "Unable to complete application test"); } - else + catch (Exception ex) { - if (version < MinimumApplicationV4Version) - { - return new ValidationFailure(string.Empty, $"Radarr version should be at least {MinimumApplicationV4Version.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); - } + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); } return null; @@ -167,30 +147,27 @@ namespace NzbDrone.Core.Applications.Radarr switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); + _logger.Error(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); + _logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); + _logger.Error(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found"); + _logger.Error(ex, "Remote indexer not found"); break; default: _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); - break; + throw; } - - throw; } catch (JsonReaderException ex) { @@ -202,15 +179,15 @@ namespace NzbDrone.Core.Applications.Radarr _logger.Error(ex, "Unable to add or update indexer"); throw; } + + return null; } private HttpRequest BuildRequest(RadarrSettings settings, string resource, HttpMethod method) { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var request = new HttpRequestBuilder(baseUrl).Resource(resource) .SetHeader("X-Api-Key", settings.ApiKey) .Build(); @@ -227,12 +204,9 @@ namespace NzbDrone.Core.Applications.Radarr { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs index 1fc6742ae..5349324f8 100644 --- a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs +++ b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; using FluentValidation.Results; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; @@ -22,8 +20,8 @@ namespace NzbDrone.Core.Applications.Readarr private readonly IReadarrV1Proxy _readarrV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _readarrV1Proxy = readarrV1Proxy; @@ -49,40 +47,12 @@ namespace NzbDrone.Core.Applications.Readarr try { - failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); } - catch (HttpException ex) + catch (WebException ex) { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); - failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid")); - break; - case HttpStatusCode.BadRequest: - _logger.Warn(ex, "Prowlarr URL is invalid"); - 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; - default: - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Readarr. {ex.Message}")); - break; - } - } - catch (JsonReaderException ex) - { - _logger.Error(ex, "Unable to parse JSON response from application"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}")); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Readarr. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Readarr")); } return new ValidationResult(failures); @@ -91,26 +61,21 @@ namespace NzbDrone.Core.Applications.Readarr public override List GetIndexerMappings() { var indexers = _readarrV1Proxy.GetIndexers(Settings) - .Where(i => i.Implementation is "Newznab" or "Torznab"); + .Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; - - if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey) { - continue; - } + var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value); - var match = AppIndexerRegex.Match(baseUrl); - - if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) - { - // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance - mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) + { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance + mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + } } } @@ -119,9 +84,7 @@ namespace NzbDrone.Core.Applications.Readarr public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -130,17 +93,9 @@ namespace NzbDrone.Core.Applications.Readarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var readarrIndexer = BuildReadarrIndexer(indexer, indexerCapabilities, indexer.Protocol); + var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol); var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); } @@ -158,27 +113,24 @@ namespace NzbDrone.Core.Applications.Readarr } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _readarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!readarrIndexer.Equals(remoteIndexer) || forceSync) + if (!readarrIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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))); @@ -186,9 +138,6 @@ namespace NzbDrone.Core.Applications.Readarr // Retain user tags not-affiliated with Prowlarr readarrIndexer.Tags.UnionWith(remoteIndexer.Tags); - // Retain user settings not-affiliated with Prowlarr - readarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId; - // Update the indexer if it still has categories that match _readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings); } @@ -204,7 +153,7 @@ namespace NzbDrone.Core.Applications.Readarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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; @@ -218,11 +167,11 @@ namespace NzbDrone.Core.Applications.Readarr } } - private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) + private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, 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", "rejectBlocklistedTorrentHashesWhileGrabbing" }; + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -248,7 +197,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(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + readarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); if (indexer.Protocol == DownloadProtocol.Torrent) { @@ -256,15 +205,10 @@ 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.Any(x => x.Name == "seedCriteria.discographySeedTime")) + if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) { 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/ReadarrField.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrField.cs index 587559cfd..c615b9938 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrField.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrField.cs @@ -2,7 +2,12 @@ namespace NzbDrone.Core.Applications.Readarr { public class ReadarrField { + public int Order { get; set; } public string Name { get; set; } + public string Label { get; set; } + public string Unit { get; set; } + public string HelpText { get; set; } + public string HelpLink { get; set; } public object Value { get; set; } public string Type { get; set; } public bool Advanced { get; set; } diff --git a/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs index b26b50ae0..e4683ae01 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrIndexer.cs @@ -17,7 +17,6 @@ namespace NzbDrone.Core.Applications.Readarr public string Implementation { get; set; } public string ConfigContract { get; set; } public string InfoLink { get; set; } - public int? DownloadClientId { get; set; } public HashSet Tags { get; set; } public List Fields { get; set; } @@ -29,12 +28,9 @@ namespace NzbDrone.Core.Applications.Readarr } var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value; var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); - var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPathCompare = apiPath.Equals(otherApiPath); @@ -55,10 +51,6 @@ 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 && @@ -66,7 +58,7 @@ namespace NzbDrone.Core.Applications.Readarr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; + apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs b/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs index f789586d3..a769d8406 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs @@ -34,15 +34,12 @@ namespace NzbDrone.Core.Applications.Readarr [FieldDefinition(1, Label = "Readarr Server", HelpText = "URL used to connect to Readarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8787")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", 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), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] 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 899ef79b6..3d74a517b 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs @@ -81,20 +81,8 @@ namespace NzbDrone.Core.Applications.Readarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to add indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings) @@ -102,20 +90,8 @@ namespace NzbDrone.Core.Applications.Readarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to update indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings) @@ -123,9 +99,39 @@ namespace NzbDrone.Core.Applications.Readarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - _httpClient.Post(request); + try + { + Execute(request); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "API Key is invalid"); + return new ValidationFailure("ApiKey", "API Key is invalid"); + } + + if (ex.Response.StatusCode == HttpStatusCode.BadRequest) + { + _logger.Error(ex, "Prowlarr URL is invalid"); + return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr"); + } + + if (ex.Response.StatusCode == HttpStatusCode.SeeOther) + { + _logger.Error(ex, "Readarr returned redirect and is invalid"); + return new ValidationFailure("BaseUrl", "Readarr url is invalid, Prowlarr cannot connect to Readarr - are you missing a url base?"); + } + + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("BaseUrl", "Unable to complete application test"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); + } return null; } @@ -141,30 +147,27 @@ namespace NzbDrone.Core.Applications.Readarr switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); + _logger.Error(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); + _logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); + _logger.Error(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found"); + _logger.Error(ex, "Remote indexer not found"); break; default: _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); - break; + throw; } - - throw; } catch (JsonReaderException ex) { @@ -176,15 +179,15 @@ namespace NzbDrone.Core.Applications.Readarr _logger.Error(ex, "Unable to add or update indexer"); throw; } + + return null; } private HttpRequest BuildRequest(ReadarrSettings settings, string resource, HttpMethod method) { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var request = new HttpRequestBuilder(baseUrl).Resource(resource) .SetHeader("X-Api-Key", settings.ApiKey) .Build(); @@ -201,12 +204,9 @@ namespace NzbDrone.Core.Applications.Readarr { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs index 6e5284fc7..d079bf0c5 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; using FluentValidation.Results; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; @@ -22,8 +20,8 @@ namespace NzbDrone.Core.Applications.Sonarr private readonly ISonarrV3Proxy _sonarrV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _sonarrV3Proxy = sonarrV3Proxy; @@ -49,44 +47,12 @@ namespace NzbDrone.Core.Applications.Sonarr try { - failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); } - catch (HttpException ex) + catch (WebException ex) { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); - failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid")); - break; - case HttpStatusCode.BadRequest: - _logger.Warn(ex, "Prowlarr URL is invalid"); - 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; - case HttpStatusCode.NotFound: - _logger.Warn(ex, "Sonarr not found"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Sonarr URL is invalid, Prowlarr cannot connect to Sonarr. Is Sonarr running and accessible? Sonarr v2 is not supported.")); - break; - default: - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Sonarr. {ex.Message}")); - break; - } - } - catch (JsonReaderException ex) - { - _logger.Error(ex, "Unable to parse JSON response from application"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}")); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Sonarr. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Sonarr")); } return new ValidationResult(failures); @@ -95,26 +61,21 @@ namespace NzbDrone.Core.Applications.Sonarr public override List GetIndexerMappings() { var indexers = _sonarrV3Proxy.GetIndexers(Settings) - .Where(i => i.Implementation is "Newznab" or "Torznab"); + .Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; - - if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey) { - continue; - } + var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value); - var match = AppIndexerRegex.Match(baseUrl); - - if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) - { - // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance - mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) + { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance + mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + } } } @@ -123,10 +84,8 @@ namespace NzbDrone.Core.Applications.Sonarr public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() && - indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty() && + indexer.Capabilities.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); @@ -135,17 +94,9 @@ namespace NzbDrone.Core.Applications.Sonarr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var sonarrIndexer = BuildSonarrIndexer(indexer, indexerCapabilities, indexer.Protocol); + var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol); var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); } @@ -163,27 +114,24 @@ namespace NzbDrone.Core.Applications.Sonarr } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _sonarrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!sonarrIndexer.Equals(remoteIndexer) || forceSync) + if (!sonarrIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) + if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.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))); @@ -210,7 +158,7 @@ namespace NzbDrone.Core.Applications.Sonarr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexerCapabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) + if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.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; @@ -224,17 +172,11 @@ namespace NzbDrone.Core.Applications.Sonarr } } - private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) + private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, 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", "rejectBlocklistedTorrentHashesWhileGrabbing" }; - - if (id == 0) - { - // Ensuring backward compatibility with older versions on first sync - syncFields.AddRange(new List { "additionalParameters" }); - } + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -260,13 +202,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(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")) - { - sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value = Settings.SyncAnimeStandardFormatSearch; - } + 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())); if (indexer.Protocol == DownloadProtocol.Torrent) { @@ -274,11 +211,6 @@ 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/SonarrField.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrField.cs index f9e5fe6f9..d0350c0ce 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrField.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrField.cs @@ -2,7 +2,12 @@ namespace NzbDrone.Core.Applications.Sonarr { public class SonarrField { + public int Order { get; set; } public string Name { get; set; } + public string Label { get; set; } + public string Unit { get; set; } + public string HelpText { get; set; } + public string HelpLink { get; set; } public object Value { get; set; } public string Type { get; set; } public bool Advanced { get; set; } diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs index 698c7ed6f..e3dffaed0 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs @@ -30,21 +30,14 @@ namespace NzbDrone.Core.Applications.Sonarr } var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value; var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); var animeCats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "animeCategories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value); - var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPathCompare = apiPath.Equals(otherApiPath); - var animeStandardFormatSearch = Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch")?.Value == null ? null : (bool?)Convert.ToBoolean(Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value); - var otherAnimeStandardFormatSearch = other.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch")?.Value == null ? null : (bool?)Convert.ToBoolean(other.Fields.FirstOrDefault(x => x.Name == "animeStandardFormatSearch").Value); - var animeStandardFormatSearchCompare = animeStandardFormatSearch == otherAnimeStandardFormatSearch; - var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders; @@ -61,10 +54,6 @@ 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 && @@ -72,7 +61,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 && rejectBlocklistedTorrentHashesWhileGrabbingCompare; + apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs index 95b52bab0..84b9ff9c6 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Applications.Sonarr { ProwlarrUrl = "http://localhost:9696"; BaseUrl = "http://localhost:8989"; - SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050, 5090 }; + SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050 }; AnimeSyncCategories = new[] { 5070 }; } @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Applications.Sonarr [FieldDefinition(1, Label = "Sonarr Server", HelpText = "URL used to connect to Sonarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8989")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Sonarr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Sonarr 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")] @@ -43,12 +43,6 @@ 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, 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() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs index f92043c99..018338386 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs @@ -23,11 +23,8 @@ namespace NzbDrone.Core.Applications.Sonarr public class SonarrV3Proxy : ISonarrV3Proxy { - private static Version MinimumApplicationVersion => new (3, 0, 5, 0); - private const string AppApiRoute = "/api/v3"; private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer"; - private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -84,20 +81,8 @@ namespace NzbDrone.Core.Applications.Sonarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to add indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings) @@ -105,20 +90,8 @@ namespace NzbDrone.Core.Applications.Sonarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to update indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings) @@ -126,18 +99,44 @@ namespace NzbDrone.Core.Applications.Sonarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); - - if (applicationVersion == null) + try { - return new ValidationFailure(string.Empty, "Failed to fetch Sonarr version"); + Execute(request); } - - if (new Version(applicationVersion) < MinimumApplicationVersion) + catch (HttpException ex) { - return new ValidationFailure(string.Empty, $"Sonarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "API Key is invalid"); + return new ValidationFailure("ApiKey", "API Key is invalid"); + } + + if (ex.Response.StatusCode == HttpStatusCode.BadRequest) + { + _logger.Error(ex, "Prowlarr URL is invalid"); + return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr"); + } + + if (ex.Response.StatusCode == HttpStatusCode.SeeOther) + { + _logger.Error(ex, "Sonarr returned redirect and is invalid"); + return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr - are you missing a url base?"); + } + + if (ex.Response.StatusCode == HttpStatusCode.NotFound) + { + _logger.Error(ex, "Sonarr not found"); + return new ValidationFailure("BaseUrl", "Sonarr url is invalid, Prowlarr cannot connect to Sonarr. Is Sonarr running and accessible? Sonarr v2 is not supported."); + } + + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("BaseUrl", "Unable to complete application test"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); } return null; @@ -154,30 +153,27 @@ namespace NzbDrone.Core.Applications.Sonarr switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); + _logger.Error(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); + _logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); + _logger.Error(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found"); + _logger.Error(ex, "Remote indexer not found"); break; default: _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); - break; + throw; } - - throw; } catch (JsonReaderException ex) { @@ -189,15 +185,15 @@ namespace NzbDrone.Core.Applications.Sonarr _logger.Error(ex, "Unable to add or update indexer"); throw; } + + return null; } private HttpRequest BuildRequest(SonarrSettings settings, string resource, HttpMethod method) { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var request = new HttpRequestBuilder(baseUrl).Resource(resource) .SetHeader("X-Api-Key", settings.ApiKey) .Build(); @@ -214,12 +210,9 @@ namespace NzbDrone.Core.Applications.Sonarr { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs index 0c149fc7c..6d16318ed 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; using FluentValidation.Results; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; @@ -22,8 +20,8 @@ namespace NzbDrone.Core.Applications.Whisparr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) - : base(appIndexerMapService, indexerFactory, logger) + public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _whisparrV3Proxy = whisparrV3Proxy; @@ -49,40 +47,12 @@ namespace NzbDrone.Core.Applications.Whisparr try { - failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings)); + failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings)); } - catch (HttpException ex) + catch (WebException ex) { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); - failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid")); - break; - case HttpStatusCode.BadRequest: - _logger.Warn(ex, "Prowlarr URL is invalid"); - 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; - default: - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Whisparr. {ex.Message}")); - break; - } - } - catch (JsonReaderException ex) - { - _logger.Error(ex, "Unable to parse JSON response from application"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}")); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to complete application test"); - failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Whisparr. {ex.Message}")); + _logger.Error(ex, "Unable to send test message"); + failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Whisparr")); } return new ValidationResult(failures); @@ -91,26 +61,21 @@ namespace NzbDrone.Core.Applications.Whisparr public override List GetIndexerMappings() { var indexers = _whisparrV3Proxy.GetIndexers(Settings) - .Where(i => i.Implementation is "Newznab" or "Torznab"); + .Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; - - if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey) { - continue; - } + var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value); - var match = AppIndexerRegex.Match(baseUrl); - - if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) - { - // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance - mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) + { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance + mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); + } } } @@ -119,9 +84,7 @@ namespace NzbDrone.Core.Applications.Whisparr public override void AddIndexer(IndexerDefinition indexer) { - var indexerCapabilities = GetIndexerCapabilities(indexer); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty()) + if (indexer.Capabilities.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); @@ -130,17 +93,9 @@ namespace NzbDrone.Core.Applications.Whisparr _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id); - var whisparrIndexer = BuildWhisparrIndexer(indexer, indexerCapabilities, indexer.Protocol); + var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol); var remoteIndexer = _whisparrV3Proxy.AddIndexer(whisparrIndexer, Settings); - - if (remoteIndexer == null) - { - _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id); - - return; - } - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); } @@ -158,27 +113,24 @@ namespace NzbDrone.Core.Applications.Whisparr } } - public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false) + public override void UpdateIndexer(IndexerDefinition indexer) { _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, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); + var whisparrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); if (remoteIndexer != null) { - _logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id); + _logger.Debug("Remote indexer found, syncing with current settings"); - if (!whisparrIndexer.Equals(remoteIndexer) || forceSync) + if (!whisparrIndexer.Equals(remoteIndexer)) { - _logger.Debug("Syncing remote indexer with current settings"); - - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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))); @@ -186,9 +138,6 @@ namespace NzbDrone.Core.Applications.Whisparr // Retain user tags not-affiliated with Prowlarr whisparrIndexer.Tags.UnionWith(remoteIndexer.Tags); - // Retain user settings not-affiliated with Prowlarr - whisparrIndexer.DownloadClientId = remoteIndexer.DownloadClientId; - // Update the indexer if it still has categories that match _whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings); } @@ -204,7 +153,7 @@ namespace NzbDrone.Core.Applications.Whisparr { _appIndexerMapService.Delete(indexerMapping.Id); - if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) + if (indexer.Capabilities.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; @@ -218,11 +167,11 @@ namespace NzbDrone.Core.Applications.Whisparr } } - private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0) + private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, 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", "seedCriteria.seasonPackSeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; + var syncFields = new[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; var newznab = schemas.First(i => i.Implementation == "Newznab"); var torznab = schemas.First(i => i.Implementation == "Torznab"); @@ -248,23 +197,13 @@ 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(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); + whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.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/WhisparrField.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrField.cs index 2bdafe2f8..e3b1139b1 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrField.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrField.cs @@ -2,7 +2,12 @@ namespace NzbDrone.Core.Applications.Whisparr { public class WhisparrField { + public int Order { get; set; } public string Name { get; set; } + public string Label { get; set; } + public string Unit { get; set; } + public string HelpText { get; set; } + public string HelpLink { get; set; } public object Value { get; set; } public string Type { get; set; } public bool Advanced { get; set; } diff --git a/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs index e8e6f8150..b1d720360 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrIndexer.cs @@ -17,7 +17,6 @@ namespace NzbDrone.Core.Applications.Whisparr public string Implementation { get; set; } public string ConfigContract { get; set; } public string InfoLink { get; set; } - public int? DownloadClientId { get; set; } public HashSet Tags { get; set; } public List Fields { get; set; } @@ -29,12 +28,9 @@ namespace NzbDrone.Core.Applications.Whisparr } var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value; var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); - var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPathCompare = apiPath.Equals(otherApiPath); @@ -47,18 +43,10 @@ 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 && @@ -66,7 +54,7 @@ namespace NzbDrone.Core.Applications.Whisparr other.Implementation == Implementation && other.Priority == Priority && other.Id == Id && - apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare && rejectBlocklistedTorrentHashesWhileGrabbingCompare; + apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs b/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs index 0dfafc166..3f888b769 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs @@ -34,15 +34,12 @@ namespace NzbDrone.Core.Applications.Whisparr [FieldDefinition(1, Label = "Whisparr Server", HelpText = "URL used to connect to Whisparr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:6969")] public string BaseUrl { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", 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), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)] + [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")] 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 e2ee60524..4a80cbed5 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs @@ -81,18 +81,8 @@ namespace NzbDrone.Core.Applications.Whisparr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return Execute(request); } public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings) @@ -100,20 +90,8 @@ namespace NzbDrone.Core.Applications.Whisparr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - try - { - return ExecuteIndexerRequest(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) - { - _logger.Debug("Retrying to update indexer forcefully"); - - request.Url = request.Url.AddQueryParam("forceSave", "true"); - - return ExecuteIndexerRequest(request); - } + return ExecuteIndexerRequest(request); } public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings) @@ -121,9 +99,39 @@ namespace NzbDrone.Core.Applications.Whisparr var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); - _httpClient.Post(request); + try + { + Execute(request); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "API Key is invalid"); + return new ValidationFailure("ApiKey", "API Key is invalid"); + } + + if (ex.Response.StatusCode == HttpStatusCode.BadRequest) + { + _logger.Error(ex, "Prowlarr URL is invalid"); + return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Whisparr cannot connect to Prowlarr"); + } + + if (ex.Response.StatusCode == HttpStatusCode.SeeOther) + { + _logger.Error(ex, "Whisparr returned redirect and is invalid"); + return new ValidationFailure("BaseUrl", "Whisparr url is invalid, Prowlarr cannot connect to Whisparr - are you missing a url base?"); + } + + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("BaseUrl", "Unable to complete application test"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("", "Unable to send test message"); + } return null; } @@ -139,30 +147,27 @@ namespace NzbDrone.Core.Applications.Whisparr switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid"); + _logger.Error(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); + _logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); + _logger.Error(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found"); + _logger.Error(ex, "Remote indexer not found"); break; default: _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); - break; + throw; } - - throw; } catch (JsonReaderException ex) { @@ -174,15 +179,15 @@ namespace NzbDrone.Core.Applications.Whisparr _logger.Error(ex, "Unable to add or update indexer"); throw; } + + return null; } private HttpRequest BuildRequest(WhisparrSettings settings, string resource, HttpMethod method) { var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl) - .Resource(resource) - .Accept(HttpAccept.Json) + var request = new HttpRequestBuilder(baseUrl).Resource(resource) .SetHeader("X-Api-Key", settings.ApiKey) .Build(); @@ -199,12 +204,9 @@ namespace NzbDrone.Core.Applications.Whisparr { var response = _httpClient.Execute(request); - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var results = JsonConvert.DeserializeObject(response.Content); - return Json.Deserialize(response.Content); + return results; } } } diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 051d045bb..97f13ee15 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -66,19 +66,12 @@ namespace NzbDrone.Core.Backup { _logger.ProgressInfo("Starting Backup"); - var backupFolder = GetBackupFolder(backupType); - _diskProvider.EnsureFolder(_backupTempFolder); - _diskProvider.EnsureFolder(backupFolder); - - if (!_diskProvider.FolderWritable(backupFolder)) - { - throw new UnauthorizedAccessException($"Backup folder {backupFolder} is not writable"); - } + _diskProvider.EnsureFolder(GetBackupFolder(backupType)); var dateNow = DateTime.Now; var backupFilename = $"prowlarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip"; - var backupPath = Path.Combine(backupFolder, backupFilename); + var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); Cleanup(); @@ -96,7 +89,7 @@ namespace NzbDrone.Core.Backup // Delete journal file created during database backup _diskProvider.DeleteFile(Path.Combine(_backupTempFolder, "prowlarr.db-journal")); - _archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, false)); + _archiveService.CreateZip(backupPath, _diskProvider.GetFiles(_backupTempFolder, SearchOption.TopDirectoryOnly)); Cleanup(); @@ -135,7 +128,7 @@ namespace NzbDrone.Core.Backup _archiveService.Extract(backupFileName, temporaryPath); - foreach (var file in _diskProvider.GetFiles(temporaryPath, false)) + foreach (var file in _diskProvider.GetFiles(temporaryPath, SearchOption.TopDirectoryOnly)) { var fileName = Path.GetFileName(file); @@ -250,7 +243,7 @@ namespace NzbDrone.Core.Backup private IEnumerable GetBackupFiles(string path) { - var files = _diskProvider.GetFiles(path, false); + var files = _diskProvider.GetFiles(path, SearchOption.TopDirectoryOnly); return files.Where(f => BackupFileRegex.IsMatch(f)); } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index f4715b203..ddd4a98bc 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -10,8 +10,6 @@ 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; @@ -39,10 +37,8 @@ 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; } @@ -57,15 +53,13 @@ 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; } - bool TrustCgnatIpAddresses { get; } + string Theme { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -78,11 +72,6 @@ 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); @@ -93,23 +82,13 @@ namespace NzbDrone.Core.Configuration ICacheManager cacheManager, IEventAggregator eventAggregator, IDiskProvider diskProvider, - IOptions postgresOptions, - IOptions authOptions, - IOptions appOptions, - IOptions serverOptions, - IOptions updateOptions, - IOptions logOptions) + IOptions postgresOptions) { _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() @@ -142,7 +121,8 @@ namespace NzbDrone.Core.Configuration continue; } - allWithDefaults.TryGetValue(configValue.Key, out var currentValue); + object currentValue; + allWithDefaults.TryGetValue(configValue.Key, out currentValue); if (currentValue == null) { continue; @@ -165,7 +145,7 @@ namespace NzbDrone.Core.Configuration { const string defaultValue = "*"; - var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue); + string bindAddress = GetValue("BindAddress", defaultValue); if (string.IsNullOrWhiteSpace(bindAddress)) { return defaultValue; @@ -175,19 +155,19 @@ namespace NzbDrone.Core.Configuration } } - public int Port => _serverOptions.Port ?? GetValueInt("Port", DEFAULT_PORT); + public int Port => GetValueInt("Port", DEFAULT_PORT); - public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", DEFAULT_SSL_PORT); + public int SslPort => GetValueInt("SslPort", DEFAULT_SSL_PORT); - public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false); + public bool EnableSsl => GetValueBoolean("EnableSsl", false); - public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true); + public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true); public string ApiKey { get { - var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey()); + var apiKey = GetValue("ApiKey", GenerateApiKey()); if (apiKey.IsNullOrWhiteSpace()) { @@ -203,7 +183,7 @@ namespace NzbDrone.Core.Configuration { get { - var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false); + var enabled = GetValueBoolean("AuthenticationEnabled", false, false); if (enabled) { @@ -211,92 +191,61 @@ namespace NzbDrone.Core.Configuration return AuthenticationType.Basic; } - return Enum.TryParse(_authOptions.Method, out var enumValue) - ? enumValue - : GetValueEnum("AuthenticationMethod", AuthenticationType.None); + return GetValueEnum("AuthenticationMethod", AuthenticationType.None); } } - public AuthenticationRequiredType AuthenticationRequired => - Enum.TryParse(_authOptions.Required, out var enumValue) - ? enumValue - : GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); - public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false); + public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); - 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); + // TODO: Change back to "master" for the first stable release. + public string Branch => GetValue("Branch", "master").ToLowerInvariant(); + 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 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 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 string UrlBase { get { - var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/'); + var urlBase = GetValue("UrlBase", "").Trim('/'); if (urlBase.IsNullOrWhiteSpace()) { return urlBase; } - return "/" + urlBase; + return "/" + urlBase.Trim('/').ToLower(); } } public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; + public string InstanceName => GetValue("InstanceName", BuildInfo.AppName); - public string InstanceName - { - get - { - var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName); + public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false); - if (instanceName.Contains(BuildInfo.AppName, StringComparison.OrdinalIgnoreCase)) - { - return instanceName; - } + public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); - return BuildInfo.AppName; - } - } + public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false); - public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", OsInfo.IsWindows, false); + public string SyslogServer => GetValue("SyslogServer", "", persist: false); - public UpdateMechanism UpdateMechanism => - Enum.TryParse(_updateOptions.Mechanism, out var enumValue) - ? enumValue - : GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); + public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false); - 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 string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant(); public int GetValueInt(string key, int defaultValue, bool persist = true) { @@ -329,13 +278,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(); }); } @@ -377,20 +326,6 @@ namespace NzbDrone.Core.Configuration } } - public void MigrateConfigFile() - { - if (!File.Exists(_configFile)) - { - return; - } - - // 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); - } - } - private void DeleteOldValues() { var xDoc = LoadConfigFile(); @@ -432,21 +367,13 @@ namespace NzbDrone.Core.Configuration throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Prowlarr will recreate it."); } - 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; + return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); } - var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); - newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); + var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); + xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); - return newXDoc; + return xDoc; } } catch (XmlException ex) @@ -470,7 +397,6 @@ namespace NzbDrone.Core.Configuration public void HandleAsync(ApplicationStartedEvent message) { - MigrateConfigFile(); EnsureDefaultConfigFile(); DeleteOldValues(); } @@ -480,7 +406,5 @@ 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 27a953823..269847f33 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -53,7 +53,8 @@ namespace NzbDrone.Core.Configuration foreach (var configValue in configValues) { - allWithDefaults.TryGetValue(configValue.Key, out var currentValue); + object currentValue; + allWithDefaults.TryGetValue(configValue.Key, out currentValue); if (currentValue == null || configValue.Value == null) { continue; @@ -77,7 +78,7 @@ namespace NzbDrone.Core.Configuration public int HistoryCleanupDays { - get { return GetValueInt("HistoryCleanupDays", 30); } + get { return GetValueInt("HistoryCleanupDays", 365); } set { SetValue("HistoryCleanupDays", value); } } @@ -181,14 +182,6 @@ namespace NzbDrone.Core.Configuration public CertificateValidationType CertificateValidation => GetValueEnum("CertificateValidation", CertificateValidationType.Enabled); - 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); @@ -216,7 +209,9 @@ namespace NzbDrone.Core.Configuration EnsureCache(); - if (_cache.TryGetValue(key, out var dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue)) + string dbValue; + + if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue)) { return dbValue; } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 5fa2ed005..4f37b8d51 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -55,6 +55,5 @@ namespace NzbDrone.Core.Configuration bool LogIndexerResponse { get; set; } CertificateValidationType CertificateValidation { get; } - string ApplicationUrl { get; } } } diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 796e277b7..659a69e7f 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -16,7 +16,6 @@ namespace NzbDrone.Core.Datastore { IEnumerable All(); int Count(); - TModel Find(int id); TModel Get(int id); TModel Insert(TModel model); TModel Update(TModel model); @@ -88,16 +87,9 @@ namespace NzbDrone.Core.Datastore return Query(Builder()); } - public TModel Find(int id) - { - var model = Query(x => x.Id == id).FirstOrDefault(); - - return model; - } - public TModel Get(int id) { - var model = Find(id); + var model = Query(x => x.Id == id).FirstOrDefault(); if (model == null) { @@ -204,7 +196,7 @@ namespace NzbDrone.Core.Datastore using (var conn = _database.OpenConnection()) { - using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) + using (IDbTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { foreach (var model in models) { @@ -254,7 +246,7 @@ namespace NzbDrone.Core.Datastore protected void Delete(SqlBuilder builder) { - var sql = builder.AddDeleteTemplate(typeof(TModel)); + var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery(); using (var conn = _database.OpenConnection()) { diff --git a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs index 19c938737..961d060f8 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 { - DatabaseConnectionInfo MainDbConnection { get; } - DatabaseConnectionInfo LogDbConnection { get; } + string MainDbConnectionString { get; } + string LogDbConnectionString { get; } string GetDatabasePath(string connectionString); } @@ -22,15 +22,15 @@ namespace NzbDrone.Core.Datastore { _configFileProvider = configFileProvider; - MainDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : + MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : GetConnectionString(appFolderInfo.GetDatabase()); - LogDbConnection = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : + LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : GetConnectionString(appFolderInfo.GetLogDatabase()); } - public DatabaseConnectionInfo MainDbConnection { get; private set; } - public DatabaseConnectionInfo LogDbConnection { get; private set; } + public string MainDbConnectionString { get; private set; } + public string LogDbConnectionString { get; private set; } public string GetDatabasePath(string connectionString) { @@ -39,40 +39,37 @@ namespace NzbDrone.Core.Datastore return connectionBuilder.DataSource; } - private static DatabaseConnectionInfo GetConnectionString(string dbPath) + private static string GetConnectionString(string dbPath) { - var connectionBuilder = new SQLiteConnectionStringBuilder - { - DataSource = dbPath, - CacheSize = (int)-20000, - DateTimeKind = DateTimeKind.Utc, - JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal, - Pooling = true, - Version = 3, - BusyTimeout = 100 - }; + var connectionBuilder = new SQLiteConnectionStringBuilder(); + + connectionBuilder.DataSource = dbPath; + connectionBuilder.CacheSize = (int)-20000; + connectionBuilder.DateTimeKind = DateTimeKind.Utc; + connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal; + connectionBuilder.Pooling = true; + connectionBuilder.Version = 3; if (OsInfo.IsOsx) { connectionBuilder.Add("Full FSync", true); } - return new DatabaseConnectionInfo(DatabaseType.SQLite, connectionBuilder.ConnectionString); + return connectionBuilder.ConnectionString; } - private DatabaseConnectionInfo GetPostgresConnectionString(string dbName) + private string GetPostgresConnectionString(string dbName) { - var connectionBuilder = new NpgsqlConnectionStringBuilder - { - Database = dbName, - Host = _configFileProvider.PostgresHost, - Username = _configFileProvider.PostgresUser, - Password = _configFileProvider.PostgresPassword, - Port = _configFileProvider.PostgresPort, - Enlist = false - }; + var connectionBuilder = new NpgsqlConnectionStringBuilder(); - return new DatabaseConnectionInfo(DatabaseType.PostgreSQL, connectionBuilder.ConnectionString); + 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; } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs b/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs index 790464f3f..e2f77c6f0 100644 --- a/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Converters } string contract; - using (var body = JsonDocument.Parse(stringValue)) + using (JsonDocument body = JsonDocument.Parse(stringValue)) { contract = body.RootElement.GetProperty("name").GetString(); } diff --git a/src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs b/src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs index df77c8f96..484ca108e 100644 --- a/src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs @@ -3,6 +3,7 @@ using System.Data; using System.Text.Json; using System.Text.Json.Serialization; using Dapper; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Datastore.Converters { diff --git a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs index fdcb227c6..902a26009 100644 --- a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs @@ -2,17 +2,18 @@ using System; using System.Data; using Dapper; -namespace NzbDrone.Core.Datastore.Converters; - -public class TimeSpanConverter : SqlMapper.TypeHandler +namespace NzbDrone.Core.Datastore.Converters { - public override void SetValue(IDbDataParameter parameter, TimeSpan value) + public class DapperTimeSpanConverter : SqlMapper.TypeHandler { - parameter.Value = value.ToString(); - } + 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; + public override TimeSpan Parse(object value) + { + return TimeSpan.Parse((string)value); + } } } diff --git a/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs b/src/NzbDrone.Core/Datastore/CorruptDatabaseException.cs index a8403187a..82b4065f7 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(innerException, message, args) + : base(message, innerException, args) { } public CorruptDatabaseException(string message, Exception innerException) - : base(innerException, message) + : base(message, innerException) { } } diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index 741a22f0b..887039bcb 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -2,6 +2,7 @@ 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; @@ -51,8 +52,9 @@ namespace NzbDrone.Core.Datastore { using var db = _datamapperFactory(); var dbConnection = db as DbConnection; + var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", ""); - return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion); + return new Version(version); } } diff --git a/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs b/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs deleted file mode 100644 index 5b53f086f..000000000 --- a/src/NzbDrone.Core/Datastore/DatabaseConnectionInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index ffc77cf18..000000000 --- a/src/NzbDrone.Core/Datastore/DatabaseVersionParser.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 0122757a7..38702abc4 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -2,13 +2,13 @@ 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; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore @@ -61,22 +61,22 @@ namespace NzbDrone.Core.Datastore public IDatabase Create(MigrationContext migrationContext) { - DatabaseConnectionInfo connectionInfo; + string connectionString; switch (migrationContext.MigrationType) { case MigrationType.Main: { - connectionInfo = _connectionStringFactory.MainDbConnection; - CreateMain(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType); + connectionString = _connectionStringFactory.MainDbConnectionString; + CreateMain(connectionString, migrationContext); break; } case MigrationType.Log: { - connectionInfo = _connectionStringFactory.LogDbConnection; - CreateLog(connectionInfo.ConnectionString, migrationContext, connectionInfo.DatabaseType); + connectionString = _connectionStringFactory.LogDbConnectionString; + CreateLog(connectionString, migrationContext); break; } @@ -91,14 +91,14 @@ namespace NzbDrone.Core.Datastore { DbConnection conn; - if (connectionInfo.DatabaseType == DatabaseType.SQLite) + if (connectionString.Contains(".db")) { conn = SQLiteFactory.Instance.CreateConnection(); - conn.ConnectionString = connectionInfo.ConnectionString; + conn.ConnectionString = connectionString; } else { - conn = new NpgsqlConnection(connectionInfo.ConnectionString); + conn = new NpgsqlConnection(connectionString); } conn.Open(); @@ -108,12 +108,12 @@ namespace NzbDrone.Core.Datastore return db; } - private void CreateMain(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) + private void CreateMain(string connectionString, MigrationContext migrationContext) { try { _restoreDatabaseService.Restore(); - _migrationController.Migrate(connectionString, migrationContext, databaseType); + _migrationController.Migrate(connectionString, migrationContext); } catch (SQLiteException e) { @@ -136,17 +136,15 @@ 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, databaseType); - return; + _migrationController.Migrate(connectionString, migrationContext); } catch (Exception ex) { if (--retryCount > 0) { + System.Threading.Thread.Sleep(5000); continue; } @@ -165,11 +163,11 @@ namespace NzbDrone.Core.Datastore } } - private void CreateLog(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) + private void CreateLog(string connectionString, MigrationContext migrationContext) { try { - _migrationController.Migrate(connectionString, migrationContext, databaseType); + _migrationController.Migrate(connectionString, migrationContext); } catch (SQLiteException e) { @@ -189,7 +187,7 @@ namespace NzbDrone.Core.Datastore Logger.Error("Unable to recreate logging database automatically. It will need to be removed manually."); } - _migrationController.Migrate(connectionString, migrationContext, databaseType); + _migrationController.Migrate(connectionString, migrationContext); } catch (Exception e) { diff --git a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs index 67e251805..c5e31f92c 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs @@ -8,12 +8,6 @@ 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; @@ -22,12 +16,6 @@ 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 deleted file mode 100644 index 93bfc0afc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/000_database_engine_version_check.cs +++ /dev/null @@ -1,69 +0,0 @@ -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/021_localization_setting_to_string.cs b/src/NzbDrone.Core/Datastore/Migration/021_localization_setting_to_string.cs index 03094a3f6..3cd0cc4c6 100644 --- a/src/NzbDrone.Core/Datastore/Migration/021_localization_setting_to_string.cs +++ b/src/NzbDrone.Core/Datastore/Migration/021_localization_setting_to_string.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data; using FluentMigrator; diff --git a/src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs b/src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs index 09e9179e6..a35a51d58 100644 --- a/src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs +++ b/src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data; using Dapper; diff --git a/src/NzbDrone.Core/Datastore/Migration/031_apprise_server_url.cs b/src/NzbDrone.Core/Datastore/Migration/031_apprise_server_url.cs deleted file mode 100644 index f49586e7e..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/031_apprise_server_url.cs +++ /dev/null @@ -1,60 +0,0 @@ -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(031)] - public class apprise_server_url : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(MigrateToServerUrl); - } - - private void MigrateToServerUrl(IDbConnection conn, IDbTransaction tran) - { - var updatedNotifications = new List(); - - using (var selectCommand = conn.CreateCommand()) - { - selectCommand.Transaction = tran; - selectCommand.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Apprise'"; - - using var reader = selectCommand.ExecuteReader(); - - while (reader.Read()) - { - var id = reader.GetInt32(0); - var settings = reader.GetString(1); - - if (!string.IsNullOrWhiteSpace(settings)) - { - var jsonObject = Json.Deserialize(settings); - - if (jsonObject.ContainsKey("baseUrl")) - { - jsonObject.Add("serverUrl", jsonObject.Value("baseUrl")); - jsonObject.Remove("baseUrl"); - } - - settings = jsonObject.ToJson(); - } - - updatedNotifications.Add(new - { - Id = id, - Settings = settings - }); - } - } - - var updateNotificationsSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; - conn.Execute(updateNotificationsSql, updatedNotifications, transaction: tran); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs b/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs deleted file mode 100644 index f7c08b580..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(032)] - public class health_restored_notification : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Notifications").AddColumn("OnHealthRestored").AsBoolean().NotNullable().WithDefaultValue(false); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/033_remove_uc.cs b/src/NzbDrone.Core/Datastore/Migration/033_remove_uc.cs deleted file mode 100644 index 856454eec..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/033_remove_uc.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(033)] - public class remove_uc : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.FromTable("Indexers").Row(new { Implementation = "Usenet Crawler" }); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/034_history_fix_data_titles.cs b/src/NzbDrone.Core/Datastore/Migration/034_history_fix_data_titles.cs deleted file mode 100644 index 450c33cb4..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/034_history_fix_data_titles.cs +++ /dev/null @@ -1,72 +0,0 @@ -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(034)] - public class history_fix_data_titles : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(MigrateHistoryDataTitle); - } - - private void MigrateHistoryDataTitle(IDbConnection conn, IDbTransaction tran) - { - var updatedHistory = new List(); - - using (var selectCommand = conn.CreateCommand()) - { - selectCommand.Transaction = tran; - selectCommand.CommandText = "SELECT \"Id\", \"Data\", \"EventType\" FROM \"History\" WHERE \"EventType\" != 3"; - - using var reader = selectCommand.ExecuteReader(); - - while (reader.Read()) - { - var id = reader.GetInt32(0); - var data = reader.GetString(1); - var eventType = reader.GetInt32(2); - - if (!string.IsNullOrWhiteSpace(data)) - { - var jsonObject = Json.Deserialize(data); - - if (eventType == 1 && jsonObject.ContainsKey("title")) - { - jsonObject.Add("grabTitle", jsonObject.Value("title")); - jsonObject.Remove("title"); - } - - if (eventType != 1 && jsonObject.ContainsKey("bookTitle")) - { - jsonObject.Add("title", jsonObject.Value("bookTitle")); - jsonObject.Remove("bookTitle"); - } - - data = jsonObject.ToJson(); - - if (!jsonObject.ContainsKey("grabTitle") && !jsonObject.ContainsKey("title")) - { - continue; - } - - updatedHistory.Add(new - { - Id = id, - Data = data - }); - } - } - } - - var updateHistorySql = "UPDATE \"History\" SET \"Data\" = @Data WHERE \"Id\" = @Id"; - conn.Execute(updateHistorySql, updatedHistory, transaction: tran); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs b/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs deleted file mode 100644 index 658bbaaaa..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(035)] - public class download_client_per_indexer : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Indexers").AddColumn("DownloadClientId").AsInt32().WithDefaultValue(0); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/036_postgres_update_timestamp_columns_to_with_timezone.cs b/src/NzbDrone.Core/Datastore/Migration/036_postgres_update_timestamp_columns_to_with_timezone.cs deleted file mode 100644 index 8a2d6fe9a..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/036_postgres_update_timestamp_columns_to_with_timezone.cs +++ /dev/null @@ -1,39 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(036)] - public class postgres_update_timestamp_columns_to_with_timezone : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("ApplicationStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable(); - Alter.Table("ApplicationStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable(); - Alter.Table("ApplicationStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable(); - Alter.Table("Commands").AlterColumn("QueuedAt").AsDateTimeOffset().NotNullable(); - Alter.Table("Commands").AlterColumn("StartedAt").AsDateTimeOffset().Nullable(); - Alter.Table("Commands").AlterColumn("EndedAt").AsDateTimeOffset().Nullable(); - Alter.Table("DownloadClientStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable(); - Alter.Table("DownloadClientStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable(); - Alter.Table("DownloadClientStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable(); - Alter.Table("History").AlterColumn("Date").AsDateTimeOffset().NotNullable(); - Alter.Table("IndexerDefinitionVersions").AlterColumn("LastUpdated").AsDateTimeOffset().Nullable(); - Alter.Table("IndexerStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable(); - Alter.Table("IndexerStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable(); - Alter.Table("IndexerStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable(); - Alter.Table("IndexerStatus").AlterColumn("CookiesExpirationDate").AsDateTimeOffset().Nullable(); - Alter.Table("Indexers").AlterColumn("Added").AsDateTimeOffset().NotNullable(); - Alter.Table("ScheduledTasks").AlterColumn("LastExecution").AsDateTimeOffset().NotNullable(); - Alter.Table("ScheduledTasks").AlterColumn("LastStartTime").AsDateTimeOffset().Nullable(); - Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable(); - } - - protected override void LogDbUpgrade() - { - Alter.Table("Logs").AlterColumn("Time").AsDateTimeOffset().NotNullable(); - Alter.Table("UpdateHistory").AlterColumn("Date").AsDateTimeOffset().NotNullable(); - Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_notification_status.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_notification_status.cs deleted file mode 100644 index 478b00103..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_notification_status.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(037)] - public class add_notification_status : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("NotificationStatus") - .WithColumn("ProviderId").AsInt32().NotNullable().Unique() - .WithColumn("InitialFailure").AsDateTimeOffset().Nullable() - .WithColumn("MostRecentFailure").AsDateTimeOffset().Nullable() - .WithColumn("EscalationLevel").AsInt32().NotNullable() - .WithColumn("DisabledTill").AsDateTimeOffset().Nullable(); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/038_indexers_freeleech_only_config_contract.cs b/src/NzbDrone.Core/Datastore/Migration/038_indexers_freeleech_only_config_contract.cs deleted file mode 100644 index 7832dbf71..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/038_indexers_freeleech_only_config_contract.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(038)] - public class indexers_freeleech_only_config_contract : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Update.Table("Indexers").Set(new { ConfigContract = "HDSpaceSettings" }).Where(new { Implementation = "HDSpace" }); - Update.Table("Indexers").Set(new { ConfigContract = "ImmortalSeedSettings" }).Where(new { Implementation = "ImmortalSeed" }); - Update.Table("Indexers").Set(new { ConfigContract = "XSpeedsSettings" }).Where(new { Implementation = "XSpeeds" }); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs b/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs deleted file mode 100644 index f275bbd70..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/039_email_encryption.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index ad573bd9c..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/040_newznab_category_to_capabilities_settings.cs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 56a11c732..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/041_gazelle_freeleech_token_options.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 5a93488d5..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/042_myanonamouse_freeleech_wedge_options.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 74151de7d..1249dfd8b 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, DatabaseType databaseType); + void Migrate(string connectionString, MigrationContext migrationContext); } public class MigrationController : IMigrationController @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework _migrationLoggerProvider = migrationLoggerProvider; } - public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType) + public void Migrate(string connectionString, MigrationContext migrationContext) { var sw = Stopwatch.StartNew(); @@ -37,23 +37,22 @@ namespace NzbDrone.Core.Datastore.Migration.Framework ServiceProvider serviceProvider; - var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres"; + var db = connectionString.Contains(".db") ? "sqlite" : "postgres"; serviceProvider = new ServiceCollection() .AddLogging(b => b.AddNLog()) .AddFluentMigratorCore() - .Configure(cfg => cfg.IncludeUntaggedMaintenances = true) .ConfigureRunner( builder => builder .AddPostgres() .AddNzbDroneSQLite() .WithGlobalConnectionString(connectionString) - .ScanIn(Assembly.GetExecutingAssembly()).For.All()) + .WithMigrationsIn(Assembly.GetExecutingAssembly())) .Configure(opt => opt.Namespace = "NzbDrone.Core.Datastore.Migration") .Configure(opt => { opt.PreviewOnly = false; - opt.Timeout = TimeSpan.FromMinutes(10); + opt.Timeout = TimeSpan.FromSeconds(60); }) .Configure(cfg => { diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs index a56384466..4ae4e6e68 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs @@ -201,7 +201,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework public virtual IList ReadDbSchema() { - var tables = ReadTables(); + IList tables = ReadTables(); foreach (var table in tables) { table.Indexes = ReadIndexes(table.SchemaName, table.Name); @@ -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_%' AND name NOT LIKE '_litestream_%' ORDER BY name;"; + const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"; var dtTable = Read(sqlCommand).Tables[0]; var tableDefinitionList = new List(); @@ -264,7 +264,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework protected virtual IList ReadIndexes(string schemaName, string tableName) { var sqlCommand = string.Format(@"SELECT type, name, sql FROM sqlite_master WHERE tbl_name = '{0}' AND type = 'index' AND name NOT LIKE 'sqlite_auto%';", tableName); - var table = Read(sqlCommand).Tables[0]; + DataTable table = Read(sqlCommand).Tables[0]; IList indexes = new List(); diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 637f61b99..93f4616d4 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -69,7 +69,6 @@ namespace NzbDrone.Core.Datastore .Ignore(x => x.ImplementationName) .Ignore(i => i.SupportsOnGrab) .Ignore(i => i.SupportsOnHealthIssue) - .Ignore(i => i.SupportsOnHealthRestored) .Ignore(i => i.SupportsOnApplicationUpdate); Mapper.Entity("IndexerProxies").RegisterModel() @@ -91,9 +90,10 @@ namespace NzbDrone.Core.Datastore .Ignore(c => c.Message); Mapper.Entity("IndexerStatus").RegisterModel(); + Mapper.Entity("DownloadClientStatus").RegisterModel(); + Mapper.Entity("ApplicationStatus").RegisterModel(); - Mapper.Entity("NotificationStatus").RegisterModel(); Mapper.Entity("CustomFilters").RegisterModel(); Mapper.Entity("UpdateHistory").RegisterModel(); @@ -109,6 +109,7 @@ 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>()); @@ -122,9 +123,6 @@ 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 3085dbf63..902985371 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -5,9 +5,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -23,17 +22,16 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public Aria2(IAria2Proxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { var gid = _proxy.AddUri(Settings, magnetLink); @@ -52,7 +50,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 return hash; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { var gid = _proxy.AddTorrent(Settings, fileContent); @@ -122,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 return null; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { var gid = _proxy.AddUri(Settings, torrentLink); diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs index 74f653f76..9cc2d9793 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs @@ -2,7 +2,6 @@ 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; @@ -96,14 +95,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddUri(Aria2Settings settings, string magnet) { - var options = new Dictionary(); + var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List { magnet }); - 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; @@ -111,16 +104,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2 public string AddTorrent(Aria2Settings settings, byte[] 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(); + var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent); - 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 f90ea6306..e88bc4cc1 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs @@ -32,18 +32,15 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [FieldDefinition(1, Label = "Port", Type = FieldType.Number)] public int Port { get; set; } - [FieldDefinition(2, Label = "XmlRpcPath", Type = FieldType.Textbox)] + [FieldDefinition(2, Label = "XML RPC Path", Type = FieldType.Textbox)] public string RpcPath { get; set; } - [FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } - [FieldDefinition(4, Label = "SecretToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] + [FieldDefinition(4, Label = "Secret token", 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 9e22a7460..5f6dd7a9d 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -6,9 +6,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.Blackhole @@ -18,21 +17,20 @@ namespace NzbDrone.Core.Download.Clients.Blackhole public override bool PreferTorrentFile => true; public TorrentBlackhole(ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException("Blackhole does not support redirected indexers."); } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { if (!Settings.SaveMagnetFiles) { @@ -56,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole return null; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { var title = release.Title; diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs index 81793f947..a25d0fd4d 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs @@ -27,16 +27,15 @@ namespace NzbDrone.Core.Download.Clients.Blackhole private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] - [FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")] + [FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .torrent file")] public string TorrentFolder { get; set; } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [FieldDefinition(1, Label = "TorrentBlackholeSaveMagnetFiles", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesHelpText")] + [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)")] public bool SaveMagnetFiles { get; set; } - [FieldDefinition(2, Label = "TorrentBlackholeSaveMagnetFilesExtension", Type = FieldType.Textbox, HelpText = "TorrentBlackholeSaveMagnetFilesExtensionHelpText")] + [FieldDefinition(2, Label = "Save Magnet Files", Type = FieldType.Textbox, HelpText = "Extension to use for magnet links, defaults to '.magnet'")] 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 0b64150ee..0f0364e61 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -7,7 +7,6 @@ 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 @@ -17,9 +16,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole public UsenetBlackhole(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, localizationService, logger) + : base(httpClient, configService, diskProvider, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs index d4b011d47..cb53385e5 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs @@ -18,8 +18,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] - [FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")] + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .nzb file")] 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 90bd6ba1f..9ea829e6a 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -7,9 +7,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -21,17 +20,16 @@ namespace NzbDrone.Core.Download.Clients.Deluge public Deluge(IDelugeProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings); @@ -40,10 +38,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge throw new DownloadClientException("Deluge failed to add magnet " + magnetLink); } - _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings); - + // _proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings); var category = GetCategoryForRelease(release) ?? Settings.Category; - if (category.IsNotNullOrWhiteSpace()) { _proxy.SetTorrentLabel(actualHash, category, Settings); @@ -57,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge return actualHash.ToUpper(); } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings); @@ -66,10 +62,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge throw new DownloadClientException("Deluge failed to add torrent " + filename); } - _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings); - + // _proxy.SetTorrentSeedingConfiguration(actualHash, release.SeedConfiguration, Settings); var category = GetCategoryForRelease(release) ?? Settings.Category; - if (category.IsNotNullOrWhiteSpace()) { _proxy.SetTorrentLabel(actualHash, category, Settings); @@ -123,12 +117,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge case WebExceptionStatus.ConnectionClosed: return new NzbDroneValidationFailure("UseSsl", "Verify SSL settings") { - DetailedDescription = "Please verify your SSL configuration on both Deluge and Prowlarr." + DetailedDescription = "Please verify your SSL configuration on both Deluge and NzbDrone." }; case WebExceptionStatus.SecureChannelFailure: return new NzbDroneValidationFailure("UseSsl", "Unable to connect through SSL") { - DetailedDescription = "Prowlarr is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both Prowlarr and Deluge to not use SSL." + DetailedDescription = "Drone is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both drone and Deluge to not use SSL." }; default: return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); @@ -217,7 +211,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge return null; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs index fb6ca2a8a..45df09c8c 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.Deluge +namespace NzbDrone.Core.Download.Clients.Deluge { public class DelugeException : DownloadClientException { diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 7639ad7df..6396052c3 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge var filter = new Dictionary(); // TODO: get_torrents_status returns the files as well, which starts to cause deluge timeouts when you get enough season packs. - // var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); + //var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); var response = ProcessRequest(settings, "web.update_ui", RequiredProperties, filter); return GetTorrents(response); @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge var filter = new Dictionary(); filter.Add("label", label); - // var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); + //var response = ProcessRequest>(settings, "core.get_torrents_status", filter, new String[0]); var response = ProcessRequest(settings, "web.update_ui", RequiredProperties, filter); return GetTorrents(response); @@ -203,7 +203,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge private JsonRpcRequestBuilder BuildRequest(DelugeSettings settings) { - var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); + string url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); var requestBuilder = new JsonRpcRequestBuilder(url); requestBuilder.LogResponseContent = true; diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index 64ff55a10..bf463eb81 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -34,24 +34,22 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Deluge")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Deluge")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsUrlBaseHelpText")] - [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/json")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(6, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing items")] public int Priority { get; set; } - [FieldDefinition(7, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrentStatus.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrentStatus.cs index 52114f241..e9f5a6a70 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrentStatus.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrentStatus.cs @@ -1,6 +1,6 @@ -namespace NzbDrone.Core.Download.Clients.Deluge +namespace NzbDrone.Core.Download.Clients.Deluge { - public class DelugeTorrentStatus + internal class DelugeTorrentStatus { public const string Paused = "Paused"; public const string Queued = "Queued"; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApi.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApi.cs index 9b3cbfc33..8fcefdd51 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApi.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApi.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.DownloadStation +namespace NzbDrone.Core.Download.Clients.DownloadStation { public enum DiskStationApi { @@ -6,7 +6,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation Auth, DownloadStationInfo, DownloadStationTask, - DownloadStation2Task, FileStationList, DSMInfo, } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApiInfo.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApiInfo.cs index b507747b3..60f84c672 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApiInfo.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DiskStationApiInfo.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.DownloadStation +namespace NzbDrone.Core.Download.Clients.DownloadStation { public class DiskStationApiInfo { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs deleted file mode 100644 index ef52bb7e0..000000000 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStation2Task.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.DownloadStation -{ - public class DownloadStation2Task - { - public string Username { get; set; } - - public string Id { get; set; } - - public string Title { get; set; } - - public long Size { get; set; } - - /// - /// /// Possible values are: BT, NZB, http, ftp, eMule and https - /// - public string Type { get; set; } - - public int Status { get; set; } - - public DownloadStationTaskAdditional Additional { get; set; } - - public override string ToString() - { - return this.Title; - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index fea6d8fc6..bc3e8ca1c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -36,8 +36,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Download Station")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Download Station")] public bool UseSsl { get; set; } [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -46,10 +45,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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.")] public string Category { get; set; } - [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientDownloadStationSettingsDirectoryHelpText")] + [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")] public string TvDirectory { get; set; } public DownloadStationSettings() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs index 3c5de2fb9..41faac633 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; using NzbDrone.Common.Serializer; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs index 79c893ab5..81e55569e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.DownloadStation diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs index 6cc396cd7..2538e3e10 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace NzbDrone.Core.Download.Clients.DownloadStation diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs index 46808fcbc..322296c06 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; using NzbDrone.Core.Download.Clients.DownloadStation.Responses; @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies public class DSMInfoProxy : DiskStationProxyBase, IDSMInfoProxy { public DSMInfoProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) - : base(DiskStationApi.DSMInfo, "SYNO.DSM.Info", httpClient, cacheManager, logger) + : base(DiskStationApi.DSMInfo, "SYNO.DSM.Info", httpClient, cacheManager, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs index c1507aa2e..213b3e505 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs @@ -172,14 +172,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies { if (apiInfo.NeedsAuthentication) { - if (_apiType == DiskStationApi.DownloadStation2Task) - { - requestBuilder.AddQueryParam("_sid", _sessionCache.Get(GenerateSessionCacheKey(settings), () => AuthenticateClient(settings), TimeSpan.FromHours(6))); - } - else - { - requestBuilder.AddFormParameter("_sid", _sessionCache.Get(GenerateSessionCacheKey(settings), () => AuthenticateClient(settings), TimeSpan.FromHours(6))); - } + requestBuilder.AddFormParameter("_sid", _sessionCache.Get(GenerateSessionCacheKey(settings), () => AuthenticateClient(settings), TimeSpan.FromHours(6))); } requestBuilder.AddFormParameter("api", apiInfo.Name); @@ -249,14 +242,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies if (info == null) { - if (api == DiskStationApi.DownloadStation2Task) - { - _logger.Warn("Info of {0} not found on {1}:{2}", api, settings.Host, settings.Port); - } - else - { - throw new DownloadClientException("Info of {0} not found on {1}:{2}", api, settings.Host, settings.Port); - } + throw new DownloadClientException("Info of {0} not found on {1}:{2}", api, settings.Host, settings.Port); } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs index 5fe44ddfd..1723fcc80 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationInfoProxy.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies public class DownloadStationInfoProxy : DiskStationProxyBase, IDownloadStationInfoProxy { public DownloadStationInfoProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) - : base(DiskStationApi.DownloadStationInfo, "SYNO.DownloadStation.Info", httpClient, cacheManager, logger) + : base(DiskStationApi.DownloadStationInfo, "SYNO.DownloadStation.Info", httpClient, cacheManager, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxy.cs similarity index 79% rename from src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs rename to src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxy.cs index 13e5131fb..5ae04a152 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxy.cs @@ -9,18 +9,21 @@ using NzbDrone.Core.Download.Clients.DownloadStation.Responses; namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies { - public class DownloadStationTaskProxyV1 : DiskStationProxyBase, IDownloadStationTaskProxy + public interface IDownloadStationTaskProxy : IDiskStationProxy { - public DownloadStationTaskProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) + IEnumerable GetTasks(DownloadStationSettings settings); + void RemoveTask(string downloadId, DownloadStationSettings settings); + void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings); + void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings); + } + + public class DownloadStationTaskProxy : DiskStationProxyBase, IDownloadStationTaskProxy + { + public DownloadStationTaskProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) : base(DiskStationApi.DownloadStationTask, "SYNO.DownloadStation.Task", httpClient, cacheManager, logger) { } - public bool IsApiSupported(DownloadStationSettings settings) - { - return GetApiInfo(settings) != null; - } - public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings) { var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.Post); diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs deleted file mode 100644 index 1eae7930e..000000000 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxySelector.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using NLog; -using NzbDrone.Common.Cache; - -namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies -{ - public interface IDownloadStationTaskProxy : IDiskStationProxy - { - bool IsApiSupported(DownloadStationSettings settings); - IEnumerable GetTasks(DownloadStationSettings settings); - void RemoveTask(string downloadId, DownloadStationSettings settings); - void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings); - void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings); - } - - public interface IDownloadStationTaskProxySelector - { - IDownloadStationTaskProxy GetProxy(DownloadStationSettings settings); - } - - public class DownloadStationTaskProxySelector : IDownloadStationTaskProxySelector - { - private readonly ICached _proxyCache; - private readonly Logger _logger; - - private readonly IDownloadStationTaskProxy _proxyV1; - private readonly IDownloadStationTaskProxy _proxyV2; - - public DownloadStationTaskProxySelector(DownloadStationTaskProxyV1 proxyV1, DownloadStationTaskProxyV2 proxyV2, ICacheManager cacheManager, Logger logger) - { - _proxyCache = cacheManager.GetCache(GetType(), "taskProxy"); - _logger = logger; - - _proxyV1 = proxyV1; - _proxyV2 = proxyV2; - } - - public IDownloadStationTaskProxy GetProxy(DownloadStationSettings settings) - { - return GetProxyCache(settings); - } - - private IDownloadStationTaskProxy GetProxyCache(DownloadStationSettings settings) - { - var propKey = $"{settings.Host}_{settings.Port}"; - - return _proxyCache.Get(propKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0)); - } - - private IDownloadStationTaskProxy FetchProxy(DownloadStationSettings settings) - { - if (_proxyV2.IsApiSupported(settings)) - { - _logger.Trace("Using DownloadStation Task API v2"); - return _proxyV2; - } - - if (_proxyV1.IsApiSupported(settings)) - { - _logger.Trace("Using DownloadStation Task API v1"); - return _proxyV1; - } - - throw new DownloadClientException("Unable to determine DownloadStations Task API version"); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs deleted file mode 100644 index 6f81c3ac3..000000000 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationTaskProxyV2.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Download.Clients.DownloadStation.Responses; - -namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies -{ - public class DownloadStationTaskProxyV2 : DiskStationProxyBase, IDownloadStationTaskProxy - { - public DownloadStationTaskProxyV2(IHttpClient httpClient, ICacheManager cacheManager, Logger logger) - : base(DiskStationApi.DownloadStation2Task, "SYNO.DownloadStation2.Task", httpClient, cacheManager, logger) - { - } - - public bool IsApiSupported(DownloadStationSettings settings) - { - return GetApiInfo(settings) != null; - } - - public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings) - { - var requestBuilder = BuildRequest(settings, "create", 2, HttpMethod.Post); - - requestBuilder.AddFormParameter("type", "\"file\""); - requestBuilder.AddFormParameter("file", "[\"fileData\"]"); - requestBuilder.AddFormParameter("create_list", "false"); - - if (downloadDirectory.IsNotNullOrWhiteSpace()) - { - requestBuilder.AddFormParameter("destination", $"\"{downloadDirectory}\""); - } - - requestBuilder.AddFormUpload("fileData", filename, data); - - ProcessRequest(requestBuilder, $"add task from data {filename}", settings); - } - - public void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings) - { - var requestBuilder = BuildRequest(settings, "create", 2); - - requestBuilder.AddQueryParam("type", "url"); - requestBuilder.AddQueryParam("url", url); - requestBuilder.AddQueryParam("create_list", "false"); - - if (downloadDirectory.IsNotNullOrWhiteSpace()) - { - requestBuilder.AddQueryParam("destination", downloadDirectory); - } - - ProcessRequest(requestBuilder, $"add task from url {url}", settings); - } - - public IEnumerable GetTasks(DownloadStationSettings settings) - { - try - { - var result = new List(); - - var requestBuilder = BuildRequest(settings, "list", 1); - requestBuilder.AddQueryParam("additional", "detail"); - - var response = ProcessRequest(requestBuilder, "get tasks with additional detail", settings); - - if (response.Success && response.Data.Total > 0) - { - requestBuilder.AddQueryParam("additional", "transfer"); - var responseTransfer = ProcessRequest(requestBuilder, "get tasks with additional transfer", settings); - - if (responseTransfer.Success) - { - foreach (var task in response.Data.Task) - { - var taskTransfer = responseTransfer.Data.Task.Where(t => t.Id == task.Id).First(); - - var combinedTask = new DownloadStationTask - { - Username = task.Username, - Id = task.Id, - Title = task.Title, - Size = task.Size, - Status = (DownloadStationTaskStatus)task.Status, - Type = task.Type, - Additional = new DownloadStationTaskAdditional - { - Detail = task.Additional.Detail, - Transfer = taskTransfer.Additional.Transfer - } - }; - - result.Add(combinedTask); - } - } - } - - return result; - } - catch (DownloadClientException e) - { - _logger.Error(e); - return Array.Empty(); - } - } - - public void RemoveTask(string downloadId, DownloadStationSettings settings) - { - var requestBuilder = BuildRequest(settings, "delete", 2); - requestBuilder.AddQueryParam("id", downloadId); - requestBuilder.AddQueryParam("force_complete", "false"); - - ProcessRequest(requestBuilder, $"remove item {downloadId}", settings); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs index fbfa3b4ae..a07cc1b47 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Http; diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DSMInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DSMInfoResponse.cs index e2e81c4ca..0848bba70 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DSMInfoResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DSMInfoResponse.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationAuthResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationAuthResponse.cs index 04d6444ac..d02503a25 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationAuthResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationAuthResponse.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses +namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { public class DiskStationAuthResponse { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs index c3f9b1090..50758d3af 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { @@ -20,22 +20,16 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { 104, "The requested version does not support the functionality" }, { 105, "The logged in session does not have permission" }, { 106, "Session timeout" }, - { 107, "Session interrupted by duplicate login" }, - { 119, "SID not found" } + { 107, "Session interrupted by duplicate login" } }; AuthMessages = new Dictionary { { 400, "No such account or incorrect password" }, - { 401, "Disabled account" }, - { 402, "Denied permission" }, - { 403, "2-step authentication code required" }, - { 404, "Failed to authenticate 2-step authentication code" }, - { 406, "Enforce to authenticate with 2-factor authentication code" }, - { 407, "Blocked IP source" }, - { 408, "Expired password cannot change" }, - { 409, "Expired password" }, - { 410, "Password must be changed" } + { 401, "Account disabled" }, + { 402, "Permission denied" }, + { 403, "2-step verification code required" }, + { 404, "Failed to authenticate 2-step verification code" } }; DownloadStationTaskMessages = new Dictionary @@ -82,7 +76,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses public int Code { get; set; } - public bool SessionError => Code == 105 || Code == 106 || Code == 107 || Code == 119; + public bool SessionError => Code == 105 || Code == 106 || Code == 107; public string GetMessage(DiskStationApi api) { @@ -91,7 +85,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses return AuthMessages[Code]; } - if ((api == DiskStationApi.DownloadStationTask || api == DiskStationApi.DownloadStation2Task) && DownloadStationTaskMessages.ContainsKey(Code)) + if (api == DiskStationApi.DownloadStationTask && DownloadStationTaskMessages.ContainsKey(Code)) { return DownloadStationTaskMessages[Code]; } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationInfoResponse.cs index c80b213e0..6c40ae75c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationInfoResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationInfoResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationResponse.cs index 6354adeb3..43c981669 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationResponse.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses +namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { public class DiskStationResponse where T : new() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStation2TaskInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStation2TaskInfoResponse.cs deleted file mode 100644 index 4d98c16d7..000000000 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStation2TaskInfoResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses -{ - public class DownloadStation2TaskInfoResponse - { - public int Offset { get; set; } - public List Task { get; set; } - public int Total { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs index 877b1890a..ebd79f3d7 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListFileInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListFileInfoResponse.cs index 927fd242b..f31d51a68 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListFileInfoResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListFileInfoResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListResponse.cs index 823465bbe..e12c60094 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/FileStationListResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/SerialNumberProvider.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/SerialNumberProvider.cs index dfeb227a2..88a419d22 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/SerialNumberProvider.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/SerialNumberProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Crypto; @@ -16,7 +16,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { private readonly IDSMInfoProxy _proxy; private readonly ILogger _logger; - private ICached _cache; public SerialNumberProvider(ICacheManager cacheManager, diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderMapping.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderMapping.cs index 354e1d50b..15946e861 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderMapping.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderMapping.cs @@ -1,4 +1,4 @@ -using NzbDrone.Common.Disk; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Download.Clients.DownloadStation { diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderResolver.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderResolver.cs index b5a308a37..25ff176f6 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderResolver.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/SharedFolderResolver.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; @@ -15,7 +15,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { private readonly IFileStationProxy _proxy; private readonly ILogger _logger; - private ICached _cache; public SharedFolderResolver(ICacheManager cacheManager, diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 97afe9480..5d3d0729e 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -7,10 +7,9 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; 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; @@ -20,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public class TorrentDownloadStation : TorrentClientBase { protected readonly IDownloadStationInfoProxy _dsInfoProxy; - protected readonly IDownloadStationTaskProxySelector _dsTaskProxySelector; + protected readonly IDownloadStationTaskProxy _dsTaskProxy; protected readonly ISharedFolderResolver _sharedFolderResolver; protected readonly ISerialNumberProvider _serialNumberProvider; protected readonly IFileStationProxy _fileStationProxy; @@ -29,17 +28,16 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation ISerialNumberProvider serialNumberProvider, IFileStationProxy fileStationProxy, IDownloadStationInfoProxy dsInfoProxy, - IDownloadStationTaskProxySelector dsTaskProxySelector, + IDownloadStationTaskProxy dsTaskProxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _dsInfoProxy = dsInfoProxy; - _dsTaskProxySelector = dsTaskProxySelector; + _dsTaskProxy = dsTaskProxy; _fileStationProxy = fileStationProxy; _sharedFolderResolver = sharedFolderResolver; _serialNumberProvider = serialNumberProvider; @@ -50,18 +48,16 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); - private IDownloadStationTaskProxy DsTaskProxy => _dsTaskProxySelector.GetProxy(Settings); - protected IEnumerable GetTasks() { - return DsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower()); + return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower()); } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); - DsTaskProxy.AddTaskFromUrl(magnetLink, GetDownloadDirectory(), Settings); + _dsTaskProxy.AddTaskFromUrl(magnetLink, GetDownloadDirectory(), Settings); var item = GetTasks().SingleOrDefault(t => t.Additional.Detail["uri"] == magnetLink); @@ -76,11 +72,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation throw new DownloadClientException("Failed to add magnet task to Download Station"); } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); - DsTaskProxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); + _dsTaskProxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); var items = GetTasks().Where(t => t.Additional.Detail["uri"] == Path.GetFileNameWithoutExtension(filename)); @@ -113,6 +109,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return torrent.Status == DownloadStationTaskStatus.Finished; } + protected bool IsCompleted(DownloadStationTask torrent) + { + return torrent.Status == DownloadStationTaskStatus.Seeding || IsFinished(torrent) || (torrent.Status == DownloadStationTaskStatus.Waiting && torrent.Size != 0 && GetRemainingSize(torrent) <= 0); + } + protected string GetMessage(DownloadStationTask torrent) { if (torrent.StatusExtra != null) @@ -134,8 +135,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected long GetRemainingSize(DownloadStationTask torrent) { var downloadedString = torrent.Additional.Transfer["size_downloaded"]; + long downloadedSize; - if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out var downloadedSize)) + if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize)) { _logger.Debug("Torrent {0} has invalid size_downloaded: {1}", torrent.Title, downloadedString); downloadedSize = 0; @@ -147,8 +149,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected TimeSpan? GetRemainingTime(DownloadStationTask torrent) { var speedString = torrent.Additional.Transfer["speed_download"]; + long downloadSpeed; - if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out var downloadSpeed)) + if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed)) { _logger.Debug("Torrent {0} has invalid speed_download: {1}", torrent.Title, speedString); downloadSpeed = 0; @@ -221,7 +224,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (DownloadClientAuthenticationException ex) { - // User could not have permission to access to downloadstation _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure(string.Empty, ex.Message); } @@ -273,7 +275,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected ValidationFailure ValidateVersion() { - var info = DsTaskProxy.GetApiInfo(Settings); + var info = _dsTaskProxy.GetApiInfo(Settings); _logger.Debug("Download Station api version information: Min {0} - Max {1}", info.MinVersion, info.MaxVersion); @@ -310,18 +312,17 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - - var destDir = GetDefaultDir(); - - if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace()) + else if (Settings.Category.IsNotNullOrWhiteSpace()) { + var destDir = GetDefaultDir(); + return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } - return destDir.TrimEnd('/'); + return null; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index e78f5f5d2..2cf42704b 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -9,7 +9,6 @@ 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; @@ -19,7 +18,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public class UsenetDownloadStation : UsenetClientBase { protected readonly IDownloadStationInfoProxy _dsInfoProxy; - protected readonly IDownloadStationTaskProxySelector _dsTaskProxySelector; + protected readonly IDownloadStationTaskProxy _dsTaskProxy; protected readonly ISharedFolderResolver _sharedFolderResolver; protected readonly ISerialNumberProvider _serialNumberProvider; protected readonly IFileStationProxy _fileStationProxy; @@ -28,16 +27,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation ISerialNumberProvider serialNumberProvider, IFileStationProxy fileStationProxy, IDownloadStationInfoProxy dsInfoProxy, - IDownloadStationTaskProxySelector dsTaskProxySelector, + IDownloadStationTaskProxy dsTaskProxy, IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, localizationService, logger) + : base(httpClient, configService, diskProvider, logger) { _dsInfoProxy = dsInfoProxy; - _dsTaskProxySelector = dsTaskProxySelector; + _dsTaskProxy = dsTaskProxy; _fileStationProxy = fileStationProxy; _sharedFolderResolver = sharedFolderResolver; _serialNumberProvider = serialNumberProvider; @@ -48,18 +46,16 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); - private IDownloadStationTaskProxy DsTaskProxy => _dsTaskProxySelector.GetProxy(Settings); - protected IEnumerable GetTasks() { - return DsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.NZB.ToString().ToLower()); + return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.NZB.ToString().ToLower()); } protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent) { var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); - DsTaskProxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); + _dsTaskProxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); var items = GetTasks().Where(t => t.Additional.Detail["uri"] == filename); @@ -131,7 +127,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (DownloadClientAuthenticationException ex) { - // User could not have permission to access to downloadstation _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure(string.Empty, ex.Message); } @@ -183,7 +178,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected ValidationFailure ValidateVersion() { - var info = DsTaskProxy.GetApiInfo(Settings); + var info = _dsTaskProxy.GetApiInfo(Settings); _logger.Debug("Download Station api version information: Min {0} - Max {1}", info.MinVersion, info.MaxVersion); @@ -216,8 +211,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected long GetRemainingSize(DownloadStationTask task) { var downloadedString = task.Additional.Transfer["size_downloaded"]; + long downloadedSize; - if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out var downloadedSize)) + if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize)) { _logger.Debug("Task {0} has invalid size_downloaded: {1}", task.Title, downloadedString); downloadedSize = 0; @@ -229,8 +225,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected long GetDownloadSpeed(DownloadStationTask task) { var speedString = task.Additional.Transfer["speed_download"]; + long downloadSpeed; - if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out var downloadSpeed)) + if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed)) { _logger.Debug("Task {0} has invalid speed_download: {1}", task.Title, speedString); downloadSpeed = 0; @@ -276,15 +273,14 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { return Settings.TvDirectory.TrimStart('/'); } - - var destDir = GetDefaultDir(); - - if (destDir.IsNotNullOrWhiteSpace() && Settings.Category.IsNotNullOrWhiteSpace()) + else if (Settings.Category.IsNotNullOrWhiteSpace()) { + var destDir = GetDefaultDir(); + return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } - return destDir.TrimEnd('/'); + return null; } 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 5d39d7b5f..f292eaf53 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -4,11 +4,9 @@ using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; 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; @@ -20,12 +18,11 @@ namespace NzbDrone.Core.Download.Clients.Flood public Flood(IFloodProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } @@ -59,21 +56,21 @@ namespace NzbDrone.Core.Download.Clients.Flood } } - return result.Where(t => t.IsNotNullOrWhiteSpace()); + return result; } public override string Name => "Flood"; public override bool SupportsCategories => true; public override ProviderMessage Message => new ProviderMessage("Prowlarr is unable to remove torrents that have finished seeding when using Flood", ProviderMessageType.Warning); - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { _proxy.AddTorrentByFile(Convert.ToBase64String(fileContent), HandleTags(release, Settings, GetCategoryForRelease(release)), Settings); return hash; } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { _proxy.AddTorrentByUrl(magnetLink, HandleTags(release, Settings, GetCategoryForRelease(release)), Settings); @@ -96,7 +93,7 @@ namespace NzbDrone.Core.Download.Clients.Flood } } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs index 1026e93e9..46f6f19cb 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs @@ -40,12 +40,10 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Flood")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Flood")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, HelpText = "DownloadClientFloodSettingsUrlBaseHelpText")] - [FieldToken(TokenField.HelpText, "UrlBase", "url", "[protocol]://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, HelpText = "Optionally adds a prefix to Flood API, such as [protocol]://[host]:[port]/[urlBase]api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -54,16 +52,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 = "DownloadClientSettingsDestinationHelpText")] + [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, HelpText = "Manually specifies download destination")] public string Destination { get; set; } - [FieldDefinition(7, Label = "Tags", Type = FieldType.Tag, HelpText = "DownloadClientFloodSettingsTagsHelpText")] + [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.")] public IEnumerable Tags { get; set; } - [FieldDefinition(8, Label = "DownloadClientFloodSettingsAdditionalTags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "DownloadClientFloodSettingsAdditionalTagsHelpText", Advanced = true)] + [FieldDefinition(8, Label = "Additional Tags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "Adds properties of media as tags. Hints are examples.", Advanced = true)] public IEnumerable AdditionalTags { get; set; } - [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadEncoding.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadEncoding.cs index 7c1ef310b..0e12570aa 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadEncoding.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadEncoding.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.FreeboxDownload +namespace NzbDrone.Core.Download.Clients.FreeboxDownload { public static class EncodingForBase64 { @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload return null; } - var textAsBytes = System.Text.Encoding.UTF8.GetBytes(text); + byte[] textAsBytes = System.Text.Encoding.UTF8.GetBytes(text); return System.Convert.ToBase64String(textAsBytes); } @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload return null; } - var textAsBytes = System.Convert.FromBase64String(encodedText); + byte[] textAsBytes = System.Convert.FromBase64String(encodedText); return System.Text.Encoding.UTF8.GetString(textAsBytes); } } diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadProxy.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadProxy.cs index ae952e2b9..1c40a3832 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadProxy.cs @@ -14,8 +14,8 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload public interface IFreeboxDownloadProxy { void Authenticate(FreeboxDownloadSettings settings); - string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings); - string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings); + string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings); + string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings); void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings); FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings); List GetTasks(FreeboxDownloadSettings settings); @@ -46,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload } } - public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings) + public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings) { var request = BuildRequest(settings).Resource("/downloads/add").Post(); request.Headers.ContentType = "application/x-www-form-urlencoded"; @@ -60,12 +60,12 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload var response = ProcessRequest(request.Build(), settings); - SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings); + SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings); return response.Result.Id; } - public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings) + public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings) { var request = BuildRequest(settings).Resource("/downloads/add").Post(); @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload var response = ProcessRequest(request.Build(), settings); - SetTorrentSettings(response.Result.Id, addPaused, addFirst, seedRatio, settings); + SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings); return response.Result.Id; } @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload return $"{settings.Host}:{settings.AppId}:{settings.AppToken}"; } - private void SetTorrentSettings(string id, bool addPaused, bool addFirst, double? seedRatio, FreeboxDownloadSettings settings) + private void SetTorrentSettings(string id, bool addPaused, bool addFirst, FreeboxDownloadSettings settings) { var request = BuildRequest(settings).Resource("/downloads/" + id).Build(); @@ -136,12 +136,6 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload body.Add("queue_pos", "1"); } - if (seedRatio != null) - { - // 0 means unlimited seeding - body.Add("stop_ratio", seedRatio); - } - if (body.Count == 0) { return; diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs index effdf37d9..3810f8e7e 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload public class FreeboxDownloadSettings : IProviderConfig { - private static readonly FreeboxDownloadSettingsValidator Validator = new (); + private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator(); public FreeboxDownloadSettings() { @@ -46,39 +46,34 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload ApiUrl = "/api/v1/"; } - [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsHostHelpText")] - [FieldToken(TokenField.HelpText, "Host", "url", "mafreebox.freebox.fr")] + [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)")] public string Host { get; set; } - [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsPortHelpText")] - [FieldToken(TokenField.HelpText, "Port", "port", 443)] + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Freebox API")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")] public bool UseSsl { get; set; } - [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/")] + [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/'")] public string ApiUrl { get; set; } - [FieldDefinition(4, Label = "DownloadClientFreeboxSettingsAppId", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsAppIdHelpText")] + [FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")] public string AppId { get; set; } - [FieldDefinition(5, Label = "DownloadClientFreeboxSettingsAppToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "DownloadClientFreeboxSettingsAppTokenHelpText")] + [FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")] public string AppToken { get; set; } - [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsDestinationHelpText")] + [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")] public string DestinationDirectory { get; set; } - [FieldDefinition(7, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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)")] public string Category { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")] public int Priority { get; set; } - [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] + [FieldDefinition(10, Label = "Add Paused", 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 00e7e06b4..8e9ce8951 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; +using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.FreeboxDownload @@ -17,47 +19,49 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } public override string Name => "Freebox Download"; + public override bool SupportsCategories => true; - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected IEnumerable GetTorrents() + { + return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower()); + } + + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { return _proxy.AddTaskFromUrl(magnetLink, GetDownloadDirectory(release).EncodeBase64(), ToBePaused(), ToBeQueuedFirst(), - GetSeedRatio(release), Settings); } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { return _proxy.AddTaskFromFile(filename, fileContent, GetDownloadDirectory(release).EncodeBase64(), ToBePaused(), ToBeQueuedFirst(), - GetSeedRatio(release), Settings); } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { return _proxy.AddTaskFromUrl(torrentLink, GetDownloadDirectory(release).EncodeBase64(), ToBePaused(), ToBeQueuedFirst(), - GetSeedRatio(release), Settings); } @@ -105,21 +109,16 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/'); - var category = GetCategoryForRelease(release) ?? Settings.Category; - - if (category.IsNotNullOrWhiteSpace()) + if (Settings.Category.IsNotNullOrWhiteSpace()) { + var category = GetCategoryForRelease(release) ?? Settings.Category; + destDir = $"{destDir}/{category}"; } return destDir; } - private bool ToBePaused() - { - return Settings.AddPaused; - } - private bool ToBeQueuedFirst() { if (Settings.Priority == (int)FreeboxDownloadPriority.First) @@ -130,14 +129,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload return false; } - private double? GetSeedRatio(TorrentInfo release) + private bool ToBePaused() { - if (release.SeedConfiguration == null || release.SeedConfiguration.Ratio == null) - { - return null; - } - - return release.SeedConfiguration.Ratio.Value * 100; + return Settings.AddPaused; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 560c40eb3..a5ca2e4fc 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -4,9 +4,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -18,12 +17,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public Hadouken(IHadoukenProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } @@ -42,14 +40,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken failures.AddIfNotNull(TestGetTorrents()); } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { _proxy.AddTorrentUri(Settings, magnetLink, GetCategoryForRelease(release) ?? Settings.Category); return hash.ToUpper(); } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { return _proxy.AddTorrentFile(Settings, fileContent, GetCategoryForRelease(release) ?? Settings.Category).ToUpper(); } @@ -99,7 +97,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken return null; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index c26923afb..c8e6f7763 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -39,13 +39,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Hadouken")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Hadouken")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -54,7 +51,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [FieldDefinition(6, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Default fallback category if no mapped category exists for a release.")] public string Category { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs index 7f0f27de7..e74b8f973 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs @@ -15,7 +15,8 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters { var result = reader.Value.ToString().Replace("_", string.Empty); - Enum.TryParse(result, true, out NzbVortexLoginResultType output); + NzbVortexLoginResultType output; + Enum.TryParse(result, true, out output); return output; } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs index aa515abdb..bd63788bc 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs @@ -15,7 +15,8 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters { var result = reader.Value.ToString().Replace("_", string.Empty); - Enum.TryParse(result, true, out NzbVortexResultType output); + NzbVortexResultType output; + Enum.TryParse(result, true, out output); return output; } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index a87460d71..e1088780d 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -7,7 +7,6 @@ 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; @@ -21,9 +20,8 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, localizationService, logger) + : base(httpClient, configService, diskProvider, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 1f276fa25..2dae1ac49 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -41,18 +41,16 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [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")] + [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")] public string UrlBase { get; set; } - [FieldDefinition(3, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(5, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing items")] 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 49ada68e6..0aa160cda 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -10,7 +10,6 @@ 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; @@ -19,14 +18,15 @@ 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, localizationService, logger) + : base(httpClient, configService, diskProvider, logger) { _proxy = proxy; } @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget protected IEnumerable GetCategories(Dictionary config) { - for (var i = 1; i < 100; i++) + for (int i = 1; i < 100; i++) { var name = config.GetValueOrDefault("Category" + i + ".Name"); @@ -169,7 +169,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var config = _proxy.GetConfig(Settings); var keepHistory = config.GetValueOrDefault("KeepHistory", "7"); - if (!int.TryParse(keepHistory, NumberStyles.None, CultureInfo.InvariantCulture, out var value) || value == 0) + int value; + if (!int.TryParse(keepHistory, NumberStyles.None, CultureInfo.InvariantCulture, out value) || value == 0) { return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0") { diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 1d0b6d644..76cff3220 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -177,10 +177,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var queue = GetQueue(settings); var history = GetHistory(settings); + int nzbId; NzbgetQueueItem queueItem; NzbgetHistoryItem historyItem; - if (id.Length < 10 && int.TryParse(id, out var nzbId)) + if (id.Length < 10 && int.TryParse(id, out nzbId)) { // Download wasn't grabbed by Prowlarr, so the id is the NzbId reported by nzbget. queueItem = queue.SingleOrDefault(h => h.NzbId == nzbId); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 7f968b512..0e1cbc912 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -41,13 +41,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "NZBGet")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -56,13 +53,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority for items added from Prowlarr")] public int Priority { get; set; } - [FieldDefinition(8, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")] + [FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NZBGet version 16.0")] 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 99f80ab22..1e98734f7 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -8,7 +8,6 @@ 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; @@ -18,9 +17,8 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { public Pneumatic(IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, localizationService, logger) + : base(configService, diskProvider, logger) { } @@ -41,9 +39,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); - var downloadResponse = await indexer.Download(url); + var nzbData = await indexer.Download(url); - await File.WriteAllBytesAsync(nzbFile, downloadResponse.Data); + File.WriteAllBytes(nzbFile, nzbData); _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 6cd8a2b89..741021a3f 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 = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")] + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "This folder will need to be reachable from XBMC")] public string NzbFolder { get; set; } - [FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")] + [FieldDefinition(1, Label = "Strm Folder", Type = FieldType.Path, HelpText = ".strm files in this folder will be import by drone")] 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 3d1863784..95dd9e30b 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -6,9 +6,8 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -27,13 +26,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public QBittorrent(IQBittorrentProxySelector proxySelector, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, ICacheManager cacheManager, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxySelector = proxySelector; @@ -43,41 +41,33 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings); private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings); - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr=")) { throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled"); } - var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue); - var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); - var moveToTop = Settings.Priority == (int)QBittorrentPriority.First; + //var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue); + //var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); + var itemToTop = Settings.Priority == (int)QBittorrentPriority.First; var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; var category = GetCategoryForRelease(release) ?? Settings.Category; - Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? release.SeedConfiguration : null, Settings, category); + Proxy.AddTorrentFromUrl(magnetLink, null, Settings, category); - if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) + if (itemToTop || forceStart) { if (!WaitForTorrent(hash)) { return hash; } - if (!addHasSetShareLimits && setShareLimits) - { - try - { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings); - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash); - } - } - - if (moveToTop) + //if (!addHasSetShareLimits && setShareLimits) + //{ + // Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings); + //} + if (itemToTop) { try { @@ -105,36 +95,28 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return hash; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { - var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue); - var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); - var moveToTop = Settings.Priority == (int)QBittorrentPriority.First; + //var setShareLimits = release.SeedConfiguration != null && (release.SeedConfiguration.Ratio.HasValue || release.SeedConfiguration.SeedTime.HasValue); + //var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); + var itemToTop = Settings.Priority == (int)QBittorrentPriority.First; var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; var category = GetCategoryForRelease(release) ?? Settings.Category; - Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? release.SeedConfiguration : null, Settings, category); + Proxy.AddTorrentFromFile(filename, fileContent, null, Settings, category); - if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) + if (itemToTop || forceStart) { if (!WaitForTorrent(hash)) { return hash; } - if (!addHasSetShareLimits && setShareLimits) - { - try - { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings); - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash); - } - } - - if (moveToTop) + //if (!addHasSetShareLimits && setShareLimits) + //{ + // Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), release.SeedConfiguration, Settings); + //} + if (itemToTop) { try { @@ -164,16 +146,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent protected bool WaitForTorrent(string hash) { - var count = 10; + var count = 5; while (count != 0) { try { - if (Proxy.IsTorrentLoaded(hash.ToLower(), Settings)) - { - return true; - } + Proxy.GetTorrentProperties(hash.ToLower(), Settings); + return true; } catch { @@ -255,9 +235,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _logger.Error(ex, "Unable to test qBittorrent"); return new NzbDroneValidationFailure("Host", "Unable to connect to qBittorrent") - { - DetailedDescription = ex.Message - }; + { + DetailedDescription = ex.Message + }; } return null; @@ -277,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return null; } - var labels = Proxy.GetLabels(Settings); + Dictionary labels = Proxy.GetLabels(Settings); foreach (var category in Categories) { @@ -317,6 +297,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { var recentPriorityDefault = Settings.Priority == (int)QBittorrentPriority.Last; + if (recentPriorityDefault) + { + return null; + } + try { var config = Proxy.GetConfig(Settings); @@ -465,7 +450,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent torrent.SeedingTime = torrentProperties.SeedingTime; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs deleted file mode 100644 index 874fbff7a..000000000 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentContentLayout.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.QBittorrent -{ - public enum QBittorrentContentLayout - { - Default = 0, - Original = 1, - Subfolder = 2 - } -} diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs index 8980502fe..224a079e9 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentLabel.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.QBittorrent +namespace NzbDrone.Core.Download.Clients.QBittorrent { public class QBittorrentLabel { diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPriority.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPriority.cs index 5455fd8f6..7374fc312 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPriority.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPriority.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.QBittorrent +namespace NzbDrone.Core.Download.Clients.QBittorrent { public enum QBittorrentPriority { diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 33fcfc5ca..2460b3239 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; + using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; + namespace NzbDrone.Core.Download.Clients.QBittorrent { public interface IQBittorrentProxy @@ -12,7 +15,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent string GetVersion(QBittorrentSettings settings); QBittorrentPreferences GetConfig(QBittorrentSettings settings); List GetTorrents(QBittorrentSettings settings); - bool IsTorrentLoaded(string hash, QBittorrentSettings settings); QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings); List GetTorrentFiles(string hash, QBittorrentSettings settings); @@ -25,6 +27,8 @@ 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); } @@ -36,6 +40,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public class QBittorrentProxySelector : IQBittorrentProxySelector { + private readonly IHttpClient _httpClient; private readonly ICached> _proxyCache; private readonly Logger _logger; @@ -44,9 +49,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1, QBittorrentProxyV2 proxyV2, + IHttpClient httpClient, ICacheManager cacheManager, Logger logger) { + _httpClient = httpClient; _proxyCache = cacheManager.GetCache>(GetType()); _logger = logger; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index 7c9cc8768..8bf6c1878 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -97,23 +97,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public bool IsTorrentLoaded(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}"); - request.LogHttpError = false; - - try - { - ProcessRequest(request, settings); - - return true; - } - catch - { - return false; - } - } - public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}"); @@ -146,7 +129,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) { request.AddFormParameter("paused", true); } @@ -176,7 +159,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) { request.AddFormParameter("paused", true); } @@ -212,7 +195,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 httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) { var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel") .Post() @@ -254,7 +237,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 httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden) + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) { return; } @@ -263,6 +246,22 @@ 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") @@ -296,14 +295,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent var request = requestBuilder.Build(); request.LogResponseContent = true; - request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden }; HttpResponse response; try { response = _httpClient.Execute(request); - - if (response.StatusCode == HttpStatusCode.Forbidden) + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Forbidden) { _logger.Debug("Authentication required, logging in."); @@ -313,10 +313,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent response = _httpClient.Execute(request); } - } - catch (HttpException ex) - { - throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); + else + { + throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); + } } catch (WebException ex) { @@ -367,9 +367,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex); } + // returns "Fails." on bad login if (response.Content != "Ok.") { - // returns "Fails." on bad login _logger.Debug("qbitTorrent authentication failed."); throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent."); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index cfa7c9934..12a29c401 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -106,24 +106,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public bool IsTorrentLoaded(string hash, QBittorrentSettings settings) - { - var request = BuildRequest(settings).Resource("/api/v2/torrents/properties") - .AddQueryParam("hash", hash); - request.LogHttpError = false; - - try - { - ProcessRequest(request, settings); - - return true; - } - catch - { - return false; - } - } - public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/properties") @@ -147,12 +129,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent var request = BuildRequest(settings).Resource("/api/v2/torrents/add") .Post() .AddFormParameter("urls", torrentUrl); + if (category.IsNotNullOrWhiteSpace()) + { + request.AddFormParameter("category", category); + } - AddTorrentDownloadFormParameters(request, settings, category); + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + { + request.AddFormParameter("paused", true); + } if (seedConfiguration != null) { - AddTorrentSeedingFormParameters(request, seedConfiguration); + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); } var result = ProcessRequest(request, settings); @@ -170,11 +164,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent .Post() .AddFormUpload("torrents", fileName, fileContent); - AddTorrentDownloadFormParameters(request, settings, category); + if (category.IsNotNullOrWhiteSpace()) + { + request.AddFormParameter("category", category); + } + + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + { + request.AddFormParameter("paused", true); + } if (seedConfiguration != null) { - AddTorrentSeedingFormParameters(request, seedConfiguration); + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); } var result = ProcessRequest(request, settings); @@ -223,72 +230,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return Json.Deserialize>(ProcessRequest(request, settings)); } - private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, bool always = false) + private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2; var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2; - if (ratioLimit != -2 || always) + if (ratioLimit != -2) { request.AddFormParameter("ratioLimit", ratioLimit); } - if (seedingTimeLimit != -2 || always) + if (seedingTimeLimit != -2) { request.AddFormParameter("seedingTimeLimit", seedingTimeLimit); } } - private void AddTorrentDownloadFormParameters(HttpRequestBuilder request, QBittorrentSettings settings, string category) - { - if (category.IsNotNullOrWhiteSpace()) - { - request.AddFormParameter("category", category); - } - - // Avoid extraneous API version check if initial state is ForceStart - if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop) - { - 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) - { - request.AddFormParameter("sequentialDownload", true); - } - - if (settings.FirstAndLast) - { - 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) { var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits") .Post() .AddFormParameter("hashes", hash); - AddTorrentSeedingFormParameters(request, seedConfiguration, true); + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); try { @@ -297,7 +261,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 httpException && httpException.Response.StatusCode == HttpStatusCode.NotFound) + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) { return; } @@ -319,7 +283,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 httpException && httpException.Response.StatusCode == HttpStatusCode.Conflict) + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict) { return; } @@ -328,6 +292,22 @@ 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") @@ -361,14 +341,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent var request = requestBuilder.Build(); request.LogResponseContent = true; - request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden }; HttpResponse response; try { response = _httpClient.Execute(request); - - if (response.StatusCode == HttpStatusCode.Forbidden) + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Forbidden) { _logger.Debug("Authentication required, logging in."); @@ -378,10 +359,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent response = _httpClient.Execute(request); } - } - catch (HttpException ex) - { - throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); + else + { + throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); + } } catch (WebException ex) { @@ -437,9 +418,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex); } + // returns "Fails." on bad login if (response.Content != "Ok.") { - // returns "Fails." on bad login _logger.Debug("qbitTorrent authentication failed."); throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent."); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 8d157dc30..d0c46c0c5 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -35,12 +35,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsUseSslHelpText")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -49,24 +47,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing items")] public int Priority { get; set; } - [FieldDefinition(8, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "DownloadClientQbittorrentSettingsInitialStateHelpText")] + [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] public int InitialState { get; set; } - [FieldDefinition(9, Label = "DownloadClientQbittorrentSettingsSequentialOrder", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsSequentialOrderHelpText")] - public bool SequentialOrder { get; set; } - - [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 b8fddbc11..56c5ddf1a 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentState.cs @@ -1,16 +1,9 @@ -using NzbDrone.Core.Annotations; - namespace NzbDrone.Core.Download.Clients.QBittorrent { public enum QBittorrentState { - [FieldOption(Label = "Started")] Start = 0, - - [FieldOption(Label = "Force Started")] ForceStart = 1, - - [FieldOption(Label = "Stopped")] - Stop = 2 + Pause = 2 } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs index f29317251..246b5b558 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdPriorityTypeConverter.cs @@ -15,7 +15,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters { var queuePriority = reader.Value.ToString(); - Enum.TryParse(queuePriority, out SabnzbdPriority output); + SabnzbdPriority output; + Enum.TryParse(queuePriority, out output); return output; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdStringArrayConverter.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdStringArrayConverter.cs index b5ab193ce..bca2353a1 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdStringArrayConverter.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/JsonConverters/SabnzbdStringArrayConverter.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd.JsonConverters var stringArray = (string[])value; writer.WriteStartArray(); - for (var i = 0; i < stringArray.Length; i++) + for (int i = 0; i < stringArray.Length; i++) { writer.WriteValue(stringArray[i]); } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 246262527..584e392b2 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -9,7 +9,6 @@ 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; @@ -23,9 +22,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(httpClient, configService, diskProvider, localizationService, logger) + : base(httpClient, configService, diskProvider, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index e25a91701..61b5f9228 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,14 +7,10 @@ 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/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 41fd428df..c876850c1 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -1,6 +1,5 @@ using System; using System.Net; -using System.Net.Http; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -52,7 +51,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd request.AddFormUpload("name", filename, nzbData, "application/x-nzb"); - if (!Json.TryDeserialize(ProcessRequest(request, settings), out var response)) + SabnzbdAddResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdAddResponse(); response.Status = true; @@ -69,7 +70,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd request.AddQueryParam("cat", category); request.AddQueryParam("priority", priority); - if (!Json.TryDeserialize(ProcessRequest(request, settings), out var response)) + SabnzbdAddResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdAddResponse(); response.Status = true; @@ -92,7 +95,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { var request = BuildRequest("version", settings); - if (!Json.TryDeserialize(ProcessRequest(request, settings), out var response)) + SabnzbdVersionResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdVersionResponse(); } @@ -151,7 +156,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var request = BuildRequest("retry", settings); request.AddQueryParam("value", id); - if (!Json.TryDeserialize(ProcessRequest(request, settings), out var response)) + SabnzbdRetryResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, settings), out response)) { response = new SabnzbdRetryResponse(); response.Status = true; @@ -201,10 +208,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { throw new DownloadClientException("Unable to connect to SABnzbd, {0}", ex, ex.Message); } - catch (HttpRequestException ex) - { - throw new DownloadClientUnavailableException("Unable to connect to SABnzbd, {0}", ex, ex.Message); - } catch (WebException ex) { if (ex.Status == WebExceptionStatus.TrustFailure) @@ -222,7 +225,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd private void CheckForError(HttpResponse response) { - if (!Json.TryDeserialize(response.Content, out var result)) + SabnzbdJsonError result; + + if (!Json.TryDeserialize(response.Content, out result)) { //Handle plain text responses from SAB result = new SabnzbdJsonError(); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 70dba78b1..37df94bd5 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -50,16 +50,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Sabnzbd")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } - [FieldDefinition(4, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(4, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } [FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -68,10 +65,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing items")] 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 ad14de894..fac544a20 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -3,9 +3,8 @@ using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Transmission { @@ -13,12 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission { public Transmission(ITransmissionProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, logger) { } @@ -40,6 +38,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission } public override string Name => "Transmission"; - public override bool SupportsCategories => true; + public override bool SupportsCategories => false; } } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 977e3bfee..a4e4d90ae 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -4,9 +4,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -18,24 +17,60 @@ namespace NzbDrone.Core.Download.Clients.Transmission public TransmissionBase(ITransmissionProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected bool HasReachedSeedLimit(TransmissionTorrent torrent, double? ratio, Lazy config) { - var category = GetCategoryForRelease(release) ?? Settings.Category; - var downloadDirectory = GetDownloadDirectory(category); + var isStopped = torrent.Status == TransmissionTorrentStatus.Stopped; + var isSeeding = torrent.Status == TransmissionTorrentStatus.Seeding; - _proxy.AddTorrentFromUrl(magnetLink, downloadDirectory, Settings); - _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); + if (torrent.SeedRatioMode == 1) + { + if (isStopped && ratio.HasValue && ratio >= torrent.SeedRatioLimit) + { + return true; + } + } + else if (torrent.SeedRatioMode == 0) + { + if (isStopped && config.Value.SeedRatioLimited && ratio >= config.Value.SeedRatioLimit) + { + return true; + } + } + // Transmission doesn't support SeedTimeLimit, use/abuse seed idle limit, but only if it was set per-torrent. + if (torrent.SeedIdleMode == 1) + { + if ((isStopped || isSeeding) && torrent.SecondsSeeding > torrent.SeedIdleLimit * 60) + { + return true; + } + } + else if (torrent.SeedIdleMode == 0) + { + // The global idle limit is a real idle limit, if it's configured then 'Stopped' is enough. + if (isStopped && config.Value.IdleSeedingLimitEnabled) + { + return true; + } + } + + return false; + } + + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings); + + //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); if (Settings.Priority == (int)TransmissionPriority.First) { _proxy.MoveTorrentToTopInQueue(hash, Settings); @@ -44,14 +79,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission return hash; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { - var category = GetCategoryForRelease(release) ?? Settings.Category; - var downloadDirectory = GetDownloadDirectory(category); - - _proxy.AddTorrentFromData(fileContent, downloadDirectory, Settings); - _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); + _proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings); + //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); if (Settings.Priority == (int)TransmissionPriority.First) { _proxy.MoveTorrentToTopInQueue(hash, Settings); @@ -60,7 +92,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission return hash; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } @@ -81,14 +113,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission return outputPath + torrent.Name.Replace(":", "_"); } - protected string GetDownloadDirectory(string category) + protected string GetDownloadDirectory() { if (Settings.Directory.IsNotNullOrWhiteSpace()) { return Settings.Directory; } - if (category.IsNullOrWhiteSpace()) + if (!Settings.Category.IsNotNullOrWhiteSpace()) { return null; } @@ -96,7 +128,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission var config = _proxy.GetConfig(Settings); var destDir = config.DownloadDir; - return $"{destDir.TrimEnd('/')}/{category}"; + return $"{destDir.TrimEnd('/')}/{Settings.Category}"; } protected ValidationFailure TestConnection() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs index e987259a9..1b96ca6d3 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Transmission { diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionException.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionException.cs index fe1b01759..3b91b4ce3 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionException.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionException.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.Transmission +namespace NzbDrone.Core.Download.Clients.Transmission { public class TransmissionException : DownloadClientException { diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionPriority.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionPriority.cs index d896909b3..1cf99c501 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionPriority.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionPriority.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.Transmission +namespace NzbDrone.Core.Download.Clients.Transmission { public enum TransmissionPriority { diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 76f8684e0..1172b600a 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; 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; @@ -142,7 +141,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission private TransmissionResponse GetSessionVariables(TransmissionSettings settings) { - // Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio. + // Retrieve transmission information such as the default download directory, bandwith throttling and seed ratio. return ProcessRequest("session-get", null, settings); } @@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false) { - var authKey = $"{requestBuilder.BaseUrl}:{settings.Password}"; + var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password); var sessionId = _authSessionIDCache.Find(authKey); @@ -221,26 +220,24 @@ namespace NzbDrone.Core.Download.Clients.Transmission authLoginRequest.SuppressHttpError = true; var response = _httpClient.Execute(authLoginRequest); - - switch (response.StatusCode) + if (response.StatusCode == HttpStatusCode.MovedPermanently) { - case HttpStatusCode.MovedPermanently: - var url = response.Headers.GetSingleValue("Location"); + var url = response.Headers.GetSingleValue("Location"); - 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"); + throw new DownloadClientException("Remote site redirected to " + url); + } + else if (response.StatusCode == 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."); + if (sessionId == null) + { + throw new DownloadClientException("Remote host did not return a Session Id."); + } + } + else + { + throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission."); } _logger.Debug("Transmission authentication succeeded."); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionResponse.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionResponse.cs index 68dc7f4d2..5d16754b7 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.Transmission { diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 2a510b5e0..4d2682ee6 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -41,14 +41,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Transmission")] public bool UseSsl { get; set; } - [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/")] + [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/'")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -57,16 +53,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategorySubFolderHelpText")] + [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.")] public string Category { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")] public string Directory { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing items")] public int Priority { get; set; } - [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index 70ab8a3b9..3abb5d4e8 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.Transmission +namespace NzbDrone.Core.Download.Clients.Transmission { public class TransmissionTorrent { @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public long TotalSize { get; set; } public long LeftUntilDone { get; set; } public bool IsFinished { get; set; } - public long Eta { get; set; } + public int Eta { get; set; } public TransmissionTorrentStatus Status { get; set; } public int SecondsDownloading { get; set; } public int SecondsSeeding { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrentStatus.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrentStatus.cs index f4683bd1b..13e40f04e 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrentStatus.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrentStatus.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Download.Clients.Transmission +namespace NzbDrone.Core.Download.Clients.Transmission { public enum TransmissionTorrentStatus { diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index 3b87962bb..bd1f4c37b 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -1,10 +1,9 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Transmission; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Vuze { @@ -14,12 +13,11 @@ namespace NzbDrone.Core.Download.Clients.Vuze public Vuze(ITransmissionProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, logger) { } @@ -29,7 +27,7 @@ namespace NzbDrone.Core.Download.Clients.Vuze // - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly. // - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder. // We have to make sure the return value points to the job folder OR file. - if (outputPath.FileName == torrent.Name || torrent.FileCount > 1) + if (outputPath == default || outputPath.FileName == torrent.Name || torrent.FileCount > 1) { _logger.Trace("Vuze output directory: {0}", outputPath); } @@ -48,7 +46,8 @@ namespace NzbDrone.Core.Download.Clients.Vuze _logger.Debug("Vuze protocol version information: {0}", versionString); - if (!int.TryParse(versionString, out var version) || version < MINIMUM_SUPPORTED_PROTOCOL_VERSION) + int version; + if (!int.TryParse(versionString, out version) || version < MINIMUM_SUPPORTED_PROTOCOL_VERSION) { { return new ValidationFailure(string.Empty, "Protocol version not supported, use Vuze 5.0.0.0 or higher with Vuze Web Remote plugin."); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 628ebdf52..9f7505cfd 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -6,11 +6,10 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; 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; @@ -24,19 +23,18 @@ namespace NzbDrone.Core.Download.Clients.RTorrent public RTorrent(IRTorrentProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { var priority = (RTorrentPriority)Settings.Priority; @@ -56,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent return hash; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { var priority = (RTorrentPriority)Settings.Priority; @@ -159,7 +157,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent return false; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 8e3cd6827..e2ab37fe2 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -36,13 +36,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "rTorrent")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to ruTorrent")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -51,16 +48,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 = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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.")] public string Category { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientRTorrentSettingsDirectoryHelpText")] + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] public string Directory { get; set; } - [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing items")] public int Priority { get; set; } - [FieldDefinition(9, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")] + [FieldDefinition(9, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to ruTorrent in a stopped state")] 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 98ad41eec..9b92312d5 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; 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 @@ -17,27 +17,29 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public class UTorrent : TorrentClientBase { private readonly IUTorrentProxy _proxy; + private readonly ICached _torrentCache; public UTorrent(IUTorrentProxy proxy, + ICacheManager cacheManager, ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, logger) { _proxy = proxy; + + _torrentCache = cacheManager.GetCache(GetType(), "differentialTorrents"); } - protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) + protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink) { _proxy.AddTorrentFromUrl(magnetLink, Settings); - _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); + //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); var category = GetCategoryForRelease(release) ?? Settings.Category; - - if (category.IsNotNullOrWhiteSpace()) + if (GetCategoryForRelease(release).IsNotNullOrWhiteSpace()) { _proxy.SetTorrentLabel(hash, category, Settings); } @@ -52,13 +54,12 @@ namespace NzbDrone.Core.Download.Clients.UTorrent return hash; } - protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent) { _proxy.AddTorrentFromFile(filename, fileContent, Settings); - _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); + //_proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); var category = GetCategoryForRelease(release) ?? Settings.Category; - if (category.IsNotNullOrWhiteSpace()) { _proxy.SetTorrentLabel(hash, category, Settings); @@ -75,9 +76,6 @@ 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) @@ -128,9 +126,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _logger.Error(ex, "Failed to test uTorrent"); return new NzbDroneValidationFailure("Host", "Unable to connect to uTorrent") - { - DetailedDescription = ex.Message - }; + { + DetailedDescription = ex.Message + }; } return null; @@ -151,7 +149,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent return null; } - protected override string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) + protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink) { throw new NotImplementedException(); } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 573fa2f1d..d3b72c04d 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -34,13 +34,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] - [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "uTorrent")] + [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to uTorrent")] public bool UseSsl { get; set; } - [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")] + [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")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -49,14 +46,13 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [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")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing items")] public int Priority { get; set; } - [FieldDefinition(8, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "DownloadClientSettingsInitialStateHelpText")] - [FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")] + [FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] public int IntialState { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs index 35fec74de..027b138e0 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public object Unknown28 { get; set; } } - public class UTorrentTorrentJsonConverter : JsonConverter + internal class UTorrentTorrentJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 9a7da6fd6..9b3741587 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -8,10 +8,10 @@ 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; +using Org.BouncyCastle.Crypto.Tls; namespace NzbDrone.Core.Download { @@ -20,7 +20,6 @@ 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; } @@ -42,12 +41,10 @@ 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/DownloadClientCategory.cs b/src/NzbDrone.Core/Download/DownloadClientCategory.cs index 659a83f86..b99e6fa4f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientCategory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientCategory.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace NzbDrone.Core.Download { diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 19aedf751..f5cd9f005 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -57,11 +57,12 @@ namespace NzbDrone.Core.Download private IEnumerable FilterBlockedClients(IEnumerable clients) { - var blockedClients = _downloadClientStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); + var blockedIndexers = _downloadClientStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); foreach (var client in clients) { - if (blockedClients.TryGetValue(client.Definition.Id, out var downloadClientStatus) && downloadClientStatus.DisabledTill.HasValue) + DownloadClientStatus downloadClientStatus; + if (blockedIndexers.TryGetValue(client.Definition.Id, out downloadClientStatus)) { _logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime()); continue; @@ -75,19 +76,10 @@ namespace NzbDrone.Core.Download { var result = base.Test(definition); - if (definition.Id == 0) - { - return result; - } - - if (result == null || result.IsValid) + if ((result == null || result.IsValid) && definition.Id != 0) { _downloadClientStatusService.RecordSuccess(definition.Id); } - else - { - _downloadClientStatusService.RecordFailure(definition.Id); - } return result; } diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index c69f4a01d..3ad8f6615 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -2,14 +2,13 @@ using System.Linq; using NLog; using NzbDrone.Common.Cache; -using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Download { public interface IProvideDownloadClient { - IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0); + IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol); IEnumerable GetDownloadClients(); IDownloadClient Get(int id); } @@ -19,23 +18,17 @@ namespace NzbDrone.Core.Download private readonly Logger _logger; private readonly IDownloadClientFactory _downloadClientFactory; private readonly IDownloadClientStatusService _downloadClientStatusService; - private readonly IIndexerFactory _indexerFactory; private readonly ICached _lastUsedDownloadClient; - public DownloadClientProvider(IDownloadClientStatusService downloadClientStatusService, - IDownloadClientFactory downloadClientFactory, - IIndexerFactory indexerFactory, - ICacheManager cacheManager, - Logger logger) + public DownloadClientProvider(IDownloadClientStatusService downloadClientStatusService, IDownloadClientFactory downloadClientFactory, ICacheManager cacheManager, Logger logger) { _logger = logger; _downloadClientFactory = downloadClientFactory; _downloadClientStatusService = downloadClientStatusService; - _indexerFactory = indexerFactory; _lastUsedDownloadClient = cacheManager.GetCache(GetType(), "lastDownloadClientId"); } - public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0) + public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol) { var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList(); @@ -44,23 +37,6 @@ namespace NzbDrone.Core.Download return null; } - if (indexerId > 0) - { - var indexer = _indexerFactory.Find(indexerId); - - if (indexer is { DownloadClientId: > 0 }) - { - var client = availableProviders.SingleOrDefault(d => d.Definition.Id == indexer.DownloadClientId); - - if (client == null) - { - throw new DownloadClientUnavailableException("Indexer specified download client is not available"); - } - - return client; - } - } - var blockedProviders = new HashSet(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId)); if (blockedProviders.Any()) @@ -78,7 +54,7 @@ namespace NzbDrone.Core.Download } // Use the first priority clients first - availableProviders = availableProviders.GroupBy(v => ((DownloadClientDefinition)v.Definition).Priority) + availableProviders = availableProviders.GroupBy(v => (v.Definition as DownloadClientDefinition).Priority) .OrderBy(v => v.Key) .First().OrderBy(v => v.Definition.Id).ToList(); diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 223c24384..49085b6e4 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -1,8 +1,10 @@ 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; @@ -14,7 +16,7 @@ namespace NzbDrone.Core.Download { public interface IDownloadService { - Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, int? downloadClientId); + Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect); Task DownloadReport(string link, int indexerId, string source, string host, string title); void RecordRedirect(string link, int indexerId, string source, string host, string title); } @@ -25,6 +27,7 @@ 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; @@ -32,6 +35,7 @@ namespace NzbDrone.Core.Download IDownloadClientStatusService downloadClientStatusService, IIndexerFactory indexerFactory, IIndexerStatusService indexerStatusService, + IRateLimitService rateLimitService, IEventAggregator eventAggregator, Logger logger) { @@ -39,28 +43,23 @@ namespace NzbDrone.Core.Download _downloadClientStatusService = downloadClientStatusService; _indexerFactory = indexerFactory; _indexerStatusService = indexerStatusService; + _rateLimitService = rateLimitService; _eventAggregator = eventAggregator; _logger = logger; } - public async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, int? downloadClientId) - { - var downloadClient = downloadClientId.HasValue - ? _downloadClientProvider.Get(downloadClientId.Value) - : _downloadClientProvider.GetDownloadClient(release.DownloadProtocol, release.IndexerId); - - await SendReportToClient(release, source, host, redirect, downloadClient); - } - - private async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, IDownloadClient downloadClient) + public async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect) { var downloadTitle = release.Title; + var downloadClient = _downloadClientProvider.GetDownloadClient(release.DownloadProtocol); if (downloadClient == null) { throw new DownloadClientUnavailableException($"{release.DownloadProtocol} Download client isn't configured yet"); } + // Get the seed configuration for this release. + // remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie); var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId)); var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl) @@ -69,7 +68,6 @@ namespace NzbDrone.Core.Download DownloadClientId = downloadClient.Definition.Id, DownloadClientName = downloadClient.Definition.Name, Redirect = redirect, - Indexer = indexer, GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api }; @@ -127,7 +125,15 @@ 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 { @@ -138,21 +144,16 @@ namespace NzbDrone.Core.Download DownloadProtocol = indexer.Protocol }; - var grabEvent = new IndexerDownloadEvent(release, false, source, host, release.Title, release.DownloadUrl) + var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl) { - Indexer = indexer, GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api }; - byte[] downloadedBytes; - try { - var downloadResponse = await indexer.Download(url); - downloadedBytes = downloadResponse.Data; + downloadedBytes = await indexer.Download(url); _indexerStatusService.RecordSuccess(indexerId); grabEvent.Successful = true; - grabEvent.ElapsedTime = downloadResponse.ElapsedTime; } catch (ReleaseUnavailableException) { @@ -197,7 +198,6 @@ namespace NzbDrone.Core.Download var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl) { Redirect = true, - Indexer = indexer, GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api }; diff --git a/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs b/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs index b4571d6e8..bce66c8bc 100644 --- a/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs +++ b/src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs @@ -32,13 +32,13 @@ namespace NzbDrone.Core.Download.Extensions public static long ElementAsLong(this XElement element, XName name) { var el = element.Element(name); - return long.TryParse(el?.Value, out var value) ? value : default; + return long.TryParse(el?.Value, out long value) ? value : default; } public static int ElementAsInt(this XElement element, XName name) { var el = element.Element(name); - return int.TryParse(el?.Value, out var value) ? value : default(int); + return int.TryParse(el?.Value, out int value) ? value : default(int); } public static int GetIntResponse(this XDocument document) diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index fea33d6c6..6c7bcf862 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index 217e70a31..9a5787e0a 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -5,10 +5,10 @@ using MonoTorrent; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; 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; @@ -18,39 +18,32 @@ namespace NzbDrone.Core.Download public abstract class TorrentClientBase : DownloadClientBase where TSettings : IProviderConfig, new() { - private readonly ITorrentFileInfoReader _torrentFileInfoReader; - private readonly ISeedConfigProvider _seedConfigProvider; + protected readonly IHttpClient _httpClient; + protected readonly ITorrentFileInfoReader _torrentFileInfoReader; protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader, - ISeedConfigProvider seedConfigProvider, + IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, localizationService, logger) + : base(configService, diskProvider, logger) { + _httpClient = httpClient; _torrentFileInfoReader = torrentFileInfoReader; - _seedConfigProvider = seedConfigProvider; } public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public virtual bool PreferTorrentFile => false; - protected abstract string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink); - protected abstract string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent); - protected abstract string AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink); + protected abstract string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink); + protected abstract string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent); + protected abstract string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink); public override async Task Download(ReleaseInfo release, bool redirect, IIndexer indexer) { var torrentInfo = release as TorrentInfo; - if (torrentInfo != null) - { - // Get the seed configuration for this release. - torrentInfo.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(release); - } - string magnetUrl = null; string torrentUrl = null; @@ -74,7 +67,7 @@ namespace NzbDrone.Core.Download { try { - return await DownloadFromWebUrl(torrentInfo, indexer, torrentUrl); + return await DownloadFromWebUrl(release, indexer, torrentUrl); } catch (Exception ex) { @@ -91,7 +84,7 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(torrentInfo, magnetUrl); + return DownloadFromMagnetUrl(release, magnetUrl); } catch (NotSupportedException ex) { @@ -105,7 +98,7 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(torrentInfo, magnetUrl); + return DownloadFromMagnetUrl(release, magnetUrl); } catch (NotSupportedException ex) { @@ -120,17 +113,18 @@ namespace NzbDrone.Core.Download if (torrentUrl.IsNotNullOrWhiteSpace()) { - return await DownloadFromWebUrl(torrentInfo, indexer, torrentUrl); + return await DownloadFromWebUrl(release, indexer, torrentUrl); } } return null; } - private async Task DownloadFromWebUrl(TorrentInfo release, IIndexer indexer, string torrentUrl) + private async Task DownloadFromWebUrl(ReleaseInfo release, IIndexer indexer, string torrentUrl) { - var downloadResponse = await indexer.Download(new Uri(torrentUrl)); - var torrentFile = downloadResponse.Data; + byte[] torrentFile = null; + + torrentFile = await indexer.Download(new Uri(torrentUrl)); // handle magnet URLs if (torrentFile.Length >= 7 @@ -161,7 +155,7 @@ namespace NzbDrone.Core.Download return actualHash; } - private string DownloadFromMagnetUrl(TorrentInfo release, string magnetUrl) + private string DownloadFromMagnetUrl(ReleaseInfo release, string magnetUrl) { string hash = null; string actualHash = null; @@ -172,7 +166,9 @@ namespace NzbDrone.Core.Download } catch (FormatException ex) { - throw new ReleaseDownloadException("Failed to parse magnetlink for release '{0}': '{1}'", ex, release.Title, magnetUrl); + _logger.Error(ex, "Failed to parse magnetlink for release '{0}': '{1}'", release.Title, magnetUrl); + + return null; } if (hash != null) diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index 1e85ffbb9..2541c6059 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -5,7 +5,6 @@ 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; @@ -20,9 +19,8 @@ namespace NzbDrone.Core.Download protected UsenetClientBase(IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, localizationService, logger) + : base(configService, diskProvider, logger) { _httpClient = httpClient; } @@ -43,10 +41,12 @@ namespace NzbDrone.Core.Download var filename = StringUtil.CleanFileName(release.Title) + ".nzb"; - var downloadResponse = await indexer.Download(url); + byte[] nzbData; + + nzbData = await indexer.Download(url); _logger.Info("Adding report [{0}] to the queue.", release.Title); - return AddFromNzbFile(release, filename, downloadResponse.Data); + return AddFromNzbFile(release, filename, nzbData); } } } diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 483f52b6b..cc507b242 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -20,6 +20,26 @@ 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); @@ -75,17 +95,17 @@ namespace NzbDrone.Core } var cs = s.ToCharArray(); - var length = 0; - var i = 0; + int length = 0; + int i = 0; while (i < cs.Length) { - var charSize = 1; + int charSize = 1; if (i < (cs.Length - 1) && char.IsSurrogate(cs[i])) { charSize = 2; } - var byteSize = Encoding.UTF8.GetByteCount(cs, i, charSize); + int byteSize = Encoding.UTF8.GetByteCount(cs, i, charSize); if ((byteSize + length) <= maxLength) { i = i + charSize; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs deleted file mode 100644 index d3198b111..000000000 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using NLog; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Configuration.Events; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Localization; - -namespace NzbDrone.Core.HealthCheck.Checks -{ - [CheckOn(typeof(ApplicationStartedEvent))] - [CheckOn(typeof(ConfigSavedEvent))] - public class ApiKeyValidationCheck : HealthCheckBase - { - private const int MinimumLength = 20; - - private readonly IConfigFileProvider _configFileProvider; - private readonly Logger _logger; - - public ApiKeyValidationCheck(IConfigFileProvider configFileProvider, Logger logger, ILocalizationService localizationService) - : base(localizationService) - { - _configFileProvider = configFileProvider; - _logger = logger; - } - - public override HealthCheck Check() - { - if (_configFileProvider.ApiKey.Length < MinimumLength) - { - _logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength); - - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary { { "length", MinimumLength } }), "#invalid-api-key"); - } - - return new HealthCheck(GetType()); - } - } -} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs index 59dc79c5b..0ea4bbd3c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationLongTermStatusCheck.cs @@ -9,8 +9,6 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] - [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class ApplicationLongTermStatusCheck : HealthCheckBase { @@ -30,12 +28,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { var enabledProviders = _providerFactory.GetAvailableProviders(); var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), - i => i.Definition.Id, - s => s.ProviderId, - (i, s) => new { Provider = i, Status = s }) - .Where(p => p.Status.InitialFailure.HasValue && - p.Status.InitialFailure.Value.Before(DateTime.UtcNow.AddHours(-6))) - .ToList(); + i => i.Definition.Id, + s => s.ProviderId, + (i, s) => new { Provider = i, Status = s }) + .Where(p => p.Status.InitialFailure.HasValue && + p.Status.InitialFailure.Value.Before( + DateTime.UtcNow.AddHours(-6))) + .ToList(); if (backOffProviders.Empty()) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs index 5d9c87184..2f7c76dfe 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApplicationStatusCheck.cs @@ -9,8 +9,6 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] - [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class ApplicationStatusCheck : HealthCheckBase { @@ -28,12 +26,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { var enabledProviders = _providerFactory.GetAvailableProviders(); var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), - i => i.Definition.Id, - s => s.ProviderId, - (i, s) => new { Provider = i, Status = s }) - .Where(p => p.Status.InitialFailure.HasValue && - p.Status.InitialFailure.Value.After(DateTime.UtcNow.AddHours(-6))) - .ToList(); + i => i.Definition.Id, + s => s.ProviderId, + (i, s) => new { Provider = i, Status = s }) + .Where(p => p.Status.InitialFailure.HasValue && + p.Status.InitialFailure.Value.After( + DateTime.UtcNow.AddHours(-6))) + .ToList(); if (backOffProviders.Empty()) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs index 6be6f572d..9582ddcab 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; @@ -9,8 +8,6 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] - [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class DownloadClientStatusCheck : HealthCheckBase { @@ -40,19 +37,10 @@ namespace NzbDrone.Core.HealthCheck.Checks if (backOffProviders.Count == enabledProviders.Count) { - return new HealthCheck(GetType(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("DownloadClientStatusAllClientHealthCheckMessage"), - "#download-clients-are-unavailable-due-to-failures"); + return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("DownloadClientStatusCheckAllClientMessage"), "#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"); + 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"); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs index 0a163045e..c8781a06a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs @@ -8,7 +8,6 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerCheck : HealthCheckBase diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs deleted file mode 100644 index 592304606..000000000 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Download; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; -using NzbDrone.Core.ThingiProvider.Events; - -namespace NzbDrone.Core.HealthCheck.Checks -{ - [CheckOn(typeof(ProviderUpdatedEvent))] - [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] - [CheckOn(typeof(ProviderBulkDeletedEvent))] - [CheckOn(typeof(ProviderUpdatedEvent))] - [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] - [CheckOn(typeof(ProviderBulkDeletedEvent))] - public class IndexerDownloadClientCheck : HealthCheckBase - { - private readonly IIndexerFactory _indexerFactory; - private readonly IDownloadClientFactory _downloadClientFactory; - - public IndexerDownloadClientCheck(IIndexerFactory indexerFactory, - IDownloadClientFactory downloadClientFactory, - ILocalizationService localizationService) - : base(localizationService) - { - _indexerFactory = indexerFactory; - _downloadClientFactory = downloadClientFactory; - } - - public override HealthCheck Check() - { - var downloadClientsIds = _downloadClientFactory.All().Where(v => v.Enable).Select(v => v.Id).ToList(); - var invalidIndexers = _indexerFactory.All() - .Where(v => v.Enable && v.DownloadClientId > 0 && !downloadClientsIds.Contains(v.DownloadClientId)) - .ToList(); - - if (invalidIndexers.Any()) - { - return new HealthCheck(GetType(), - HealthCheckResult.Warning, - _localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) } - }), - "#invalid-indexer-download-client-setting"); - } - - return new HealthCheck(GetType()); - } - } -} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs index f65f44962..792fe7bd9 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -10,7 +9,6 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerLongTermStatusCheck : HealthCheckBase @@ -18,7 +16,9 @@ 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; @@ -29,12 +29,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { var enabledProviders = _providerFactory.GetAvailableProviders(); var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), - i => i.Definition.Id, - s => s.ProviderId, - (i, s) => new { Provider = i, Status = s }) - .Where(p => p.Status.InitialFailure.HasValue && - p.Status.InitialFailure.Value.Before(DateTime.UtcNow.AddHours(-6))) - .ToList(); + i => i.Definition.Id, + s => s.ProviderId, + (i, s) => new { Provider = i, Status = s }) + .Where(p => p.Status.InitialFailure.HasValue && + p.Status.InitialFailure.Value.Before( + DateTime.UtcNow.AddHours(-6))) + .ToList(); if (backOffProviders.Empty()) { @@ -45,16 +46,14 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerLongTermStatusAllUnavailableHealthCheckMessage"), + _localizationService.GetLocalizedString("IndexerLongTermStatusCheckAllClientMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - _localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } - }), + string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusCheckSingleClientMessage"), + string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#indexers-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs similarity index 50% rename from src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs rename to src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs index 39aeac49e..a78061561 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerProxies; @@ -10,11 +9,11 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderDeletedEvent))] [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] - public class IndexerProxyStatusCheck : HealthCheckBase + public class IndexerProxyCheck : HealthCheckBase { private readonly IIndexerProxyFactory _proxyFactory; - public IndexerProxyStatusCheck(IIndexerProxyFactory proxyFactory, + public IndexerProxyCheck(IIndexerProxyFactory proxyFactory, ILocalizationService localizationService) : base(localizationService) { @@ -23,32 +22,28 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var enabledProxies = _proxyFactory.GetAvailableProviders() - .Where(n => ((IndexerProxyDefinition)n.Definition).Enable) - .ToList(); + var enabledProviders = _proxyFactory.GetAvailableProviders(); - var badProxies = enabledProxies.Where(p => p.Test().IsValid == false).ToList(); + var badProxies = enabledProviders.Where(p => p.Test().IsValid == false).ToList(); - if (enabledProxies.Empty() || badProxies.Count == 0) + if (enabledProviders.Empty() || badProxies.Count == 0) { return new HealthCheck(GetType()); } - if (badProxies.Count == enabledProxies.Count) + if (badProxies.Count == enabledProviders.Count) { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerProxyStatusAllUnavailableHealthCheckMessage"), - "#indexer-proxies-are-unavailable-due-to-failures"); + _localizationService.GetLocalizedString("IndexerProxyStatusCheckAllClientMessage"), + "#proxies-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - _localizationService.GetLocalizedString("IndexerProxyStatusUnavailableHealthCheckMessage", new Dictionary - { - { "indexerProxyNames", string.Join(", ", badProxies.Select(v => v.Definition.Name)) } - }), - "#indexer-proxies-are-unavailable-due-to-failures"); + string.Format(_localizationService.GetLocalizedString("IndexerProxyStatusCheckSingleClientMessage"), + string.Join(", ", badProxies.Select(v => v.Definition.Name))), + "#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 2e8846a75..fae67980b 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -10,7 +9,6 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] [CheckOn(typeof(ProviderStatusChangedEvent))] public class IndexerStatusCheck : HealthCheckBase @@ -29,12 +27,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { var enabledProviders = _providerFactory.GetAvailableProviders(); var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), - i => i.Definition.Id, - s => s.ProviderId, - (i, s) => new { Provider = i, Status = s }) - .Where(p => p.Status.InitialFailure.HasValue && - p.Status.InitialFailure.Value.After(DateTime.UtcNow.AddHours(-6))) - .ToList(); + i => i.Definition.Id, + s => s.ProviderId, + (i, s) => new { Provider = i, Status = s }) + .Where(p => p.Status.InitialFailure.HasValue && + p.Status.InitialFailure.Value.After( + DateTime.UtcNow.AddHours(-6))) + .ToList(); if (backOffProviders.Empty()) { @@ -45,16 +44,14 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerStatusAllUnavailableHealthCheckMessage"), + _localizationService.GetLocalizedString("IndexerStatusCheckAllClientMessage"), "#indexers-are-unavailable-due-to-failures"); } return new HealthCheck(GetType(), HealthCheckResult.Warning, - _localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } - }), + string.Format(_localizationService.GetLocalizedString("IndexerStatusCheckSingleClientMessage"), + 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 2337403b8..4265b5963 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPCheck.cs @@ -11,7 +11,6 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] public class IndexerVIPCheck : HealthCheckBase { @@ -25,7 +24,7 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var indexers = _indexerFactory.Enabled(false); + var indexers = _indexerFactory.AllProviders(false); var expiringProviders = new List(); foreach (var provider in indexers) @@ -40,8 +39,12 @@ namespace NzbDrone.Core.HealthCheck.Checks var expiration = (string)vipProp.GetValue(provider.Definition.Settings); - if (expiration.IsNotNullOrWhiteSpace() && - DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7))) + if (expiration.IsNullOrWhiteSpace()) + { + continue; + } + + if (DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7))) { expiringProviders.Add(provider); } @@ -50,12 +53,10 @@ namespace NzbDrone.Core.HealthCheck.Checks if (!expiringProviders.Empty()) { return new HealthCheck(GetType(), - HealthCheckResult.Warning, - _localizationService.GetLocalizedString("IndexerVipExpiringHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", expiringProviders.Select(v => v.Definition.Name).ToArray()) } - }), - "#indexer-vip-expiring"); + HealthCheckResult.Warning, + string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiringClientMessage"), + string.Join(", ", expiringProviders.Select(v => v.Definition.Name))), + "#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 0f3dffc1e..8b0dd06e7 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerVIPExpiredCheck.cs @@ -11,7 +11,6 @@ namespace NzbDrone.Core.HealthCheck.Checks [CheckOn(typeof(ProviderAddedEvent))] [CheckOn(typeof(ProviderUpdatedEvent))] [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderBulkUpdatedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] public class IndexerVIPExpiredCheck : HealthCheckBase { @@ -25,7 +24,7 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var indexers = _indexerFactory.Enabled(false); + var indexers = _indexerFactory.AllProviders(false); var expiredProviders = new List(); foreach (var provider in indexers) @@ -40,8 +39,12 @@ namespace NzbDrone.Core.HealthCheck.Checks var expiration = (string)vipProp.GetValue(provider.Definition.Settings); - if (expiration.IsNotNullOrWhiteSpace() && - DateTime.Parse(expiration).Before(DateTime.Now)) + if (expiration.IsNullOrWhiteSpace()) + { + continue; + } + + if (DateTime.Parse(expiration).Before(DateTime.Now)) { expiredProviders.Add(provider); } @@ -50,12 +53,10 @@ namespace NzbDrone.Core.HealthCheck.Checks if (!expiredProviders.Empty()) { return new HealthCheck(GetType(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerVipExpiredHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", expiredProviders.Select(v => v.Definition.Name).ToArray()) } - }), - "#indexer-vip-expired"); + HealthCheckResult.Error, + string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"), + string.Join(", ", expiredProviders.Select(v => v.Definition.Name))), + "#indexer-vip-expired"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs similarity index 53% rename from src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs rename to src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs index d6f0ad90c..9c60ec427 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerNoDefinitionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/NoDefinitionCheck.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Definitions.Cardigann; @@ -10,12 +9,12 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(ProviderDeletedEvent))] [CheckOn(typeof(ProviderBulkDeletedEvent))] - public class IndexerNoDefinitionCheck : HealthCheckBase + public class NoDefinitionCheck : HealthCheckBase { private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService; private readonly IIndexerFactory _indexerFactory; - public IndexerNoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService) + public NoDefinitionCheck(IIndexerDefinitionUpdateService indexerDefinitionUpdateService, IIndexerFactory indexerFactory, ILocalizationService localizationService) : base(localizationService) { _indexerDefinitionUpdateService = indexerDefinitionUpdateService; @@ -24,22 +23,23 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - 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 currentDefs = _indexerDefinitionUpdateService.All(); - if (noDefinitionIndexers.Count == 0) + var noDefIndexers = _indexerFactory.AllProviders(false) + .Where(i => i.Definition.Implementation == "Cardigann" && !currentDefs.Any(d => d.File == ((CardigannSettings)i.Definition.Settings).DefinitionFile)).ToList(); + + if (noDefIndexers.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(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("IndexerNoDefinitionCheckHealthCheckMessage", new Dictionary - { - { "indexerNames", string.Join(", ", noDefinitionIndexers.Select(v => v.Definition.Name).ToArray()) } - }), + healthType, + healthMessage, "#indexers-have-no-definition"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs deleted file mode 100644 index daf5ee725..000000000 --- a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Localization; -using NzbDrone.Core.Notifications; -using NzbDrone.Core.ThingiProvider.Events; - -namespace NzbDrone.Core.HealthCheck.Checks -{ - [CheckOn(typeof(ProviderUpdatedEvent))] - [CheckOn(typeof(ProviderDeletedEvent))] - [CheckOn(typeof(ProviderStatusChangedEvent))] - public class NotificationStatusCheck : HealthCheckBase - { - private readonly INotificationFactory _providerFactory; - private readonly INotificationStatusService _providerStatusService; - - public NotificationStatusCheck(INotificationFactory providerFactory, INotificationStatusService providerStatusService, ILocalizationService localizationService) - : base(localizationService) - { - _providerFactory = providerFactory; - _providerStatusService = providerStatusService; - } - - public override HealthCheck Check() - { - var enabledProviders = _providerFactory.GetAvailableProviders(); - var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(), - i => i.Definition.Id, - s => s.ProviderId, - (i, s) => new { Provider = i, Status = s }) - .ToList(); - - if (backOffProviders.Empty()) - { - return new HealthCheck(GetType()); - } - - if (backOffProviders.Count == enabledProviders.Count) - { - return new HealthCheck(GetType(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("NotificationStatusAllClientHealthCheckMessage"), - "#notifications-are-unavailable-due-to-failures"); - } - - return new HealthCheck(GetType(), - HealthCheckResult.Warning, - _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 ba1e40657..f8f7df340 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using NLog; @@ -20,7 +19,7 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IHttpRequestBuilderFactory _cloudRequestBuilder; - public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, Logger logger, ILocalizationService localizationService) + public ProxyCheck(IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigService configService, IHttpClient client, ILocalizationService localizationService, Logger logger) : base(localizationService) { _configService = configService; @@ -32,57 +31,34 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - if (!_configService.ProxyEnabled) + if (_configService.ProxyEnabled) { - return new HealthCheck(GetType()); - } - - var addresses = Dns.GetHostAddresses(_configService.ProxyHostname); - - if (!addresses.Any()) - { - return new HealthCheck(GetType(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage", new Dictionary - { - { "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) + var addresses = Dns.GetHostAddresses(_configService.ProxyHostname); + if (!addresses.Any()) { - _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"); + return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), _configService.ProxyHostname)); } - } - catch (Exception ex) - { - _logger.Error(ex, "Proxy Health Check failed"); - return new HealthCheck(GetType(), - HealthCheckResult.Error, - _localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage", new Dictionary + 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) { - { "url", request.Url } - }), - "#proxy-failed-test"); + _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) + { + _logger.Error(ex, "Proxy Health Check failed"); + return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url)); + } } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/SystemTimeCheck.cs index d2b3acf13..8ee1f5a30 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, Logger logger, ILocalizationService localizationService) + public SystemTimeCheck(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, ILocalizationService localizationService, Logger logger) : base(localizationService) { _client = client; @@ -29,26 +29,19 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType()); } - try - { - var request = _cloudRequestBuilder.Create() - .Resource("/time") - .Build(); + var request = _cloudRequestBuilder.Create() + .Resource("/time") + .Build(); - var response = _client.Execute(request); - var result = Json.Deserialize(response.Content); - var systemTime = DateTime.UtcNow; + 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) + // +/- more than 1 day + if (Math.Abs(result.DateTimeUtc.Subtract(systemTime).TotalDays) >= 1) { - _logger.Warn(e, "Unable to verify system time"); + _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")); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 684d7f60a..b012d5c17 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -40,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck.Checks var startupFolder = _appFolderInfo.StartUpFolder; var uiFolder = Path.Combine(startupFolder, "UI"); - if (_configFileProvider.UpdateAutomatically && + if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) && _configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && !_osInfo.IsDocker) { @@ -48,12 +47,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString( - "UpdateStartupTranslocationHealthCheckMessage", - new Dictionary - { - { "startupFolder", startupFolder } - }), + string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupTranslocationMessage"), startupFolder), "#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder."); } @@ -61,13 +55,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString( - "UpdateStartupNotWritableHealthCheckMessage", - new Dictionary - { - { "startupFolder", startupFolder }, - { "userName", Environment.UserName } - }), + string.Format(_localizationService.GetLocalizedString("UpdateCheckStartupNotWritableMessage"), startupFolder, Environment.UserName), "#cannot-install-update-because-startup-folder-is-not-writable-by-the-user"); } @@ -75,30 +63,16 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - _localizationService.GetLocalizedString( - "UpdateUiNotWritableHealthCheckMessage", - new Dictionary - { - { "uiFolder", uiFolder }, - { "userName", Environment.UserName } - }), + string.Format(_localizationService.GetLocalizedString("UpdateCheckUINotWritableMessage"), uiFolder, Environment.UserName), "#cannot-install-update-because-ui-folder-is-not-writable-by-the-user"); } } if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14)) { - var latestAvailable = _checkUpdateService.AvailableUpdate(); - - if (latestAvailable != null) + if (_checkUpdateService.AvailableUpdate() != 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(), HealthCheckResult.Warning, "New update is available"); } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckFailedEvent.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckFailedEvent.cs index c92dd960a..8abb156a5 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckFailedEvent.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckFailedEvent.cs @@ -5,12 +5,12 @@ namespace NzbDrone.Core.HealthCheck public class HealthCheckFailedEvent : IEvent { public HealthCheck HealthCheck { get; private set; } - public bool IsInStartupGracePeriod { get; private set; } + public bool IsInStartupGraceperiod { get; private set; } - public HealthCheckFailedEvent(HealthCheck healthCheck, bool isInStartupGracePeriod) + public HealthCheckFailedEvent(HealthCheck healthCheck, bool isInStartupGraceperiod) { HealthCheck = healthCheck; - IsInStartupGracePeriod = isInStartupGracePeriod; + IsInStartupGraceperiod = isInStartupGraceperiod; } } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs deleted file mode 100644 index a31b63cc4..000000000 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.HealthCheck -{ - public class HealthCheckRestoredEvent : IEvent - { - public HealthCheck PreviousCheck { get; private set; } - public bool IsInStartupGracePeriod { get; private set; } - - public HealthCheckRestoredEvent(HealthCheck previousCheck, bool isInStartupGracePeriod) - { - PreviousCheck = previousCheck; - IsInStartupGracePeriod = isInStartupGracePeriod; - } - } -} diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index a9a89dc48..1538674ab 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -6,7 +6,6 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Messaging; using NzbDrone.Common.Reflection; -using NzbDrone.Common.TPL; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -28,35 +27,35 @@ namespace NzbDrone.Core.HealthCheck private readonly IProvideHealthCheck[] _startupHealthChecks; private readonly IProvideHealthCheck[] _scheduledHealthChecks; private readonly Dictionary _eventDrivenHealthChecks; + private readonly IServerSideNotificationService _serverSideNotificationService; private readonly IEventAggregator _eventAggregator; + private readonly ICacheManager _cacheManager; private readonly Logger _logger; private readonly ICached _healthCheckResults; - private readonly HashSet _pendingHealthChecks; - private readonly Debouncer _debounce; private bool _hasRunHealthChecksAfterGracePeriod; private bool _isRunningHealthChecksAfterGracePeriod; public HealthCheckService(IEnumerable healthChecks, + IServerSideNotificationService serverSideNotificationService, IEventAggregator eventAggregator, ICacheManager cacheManager, - IDebounceManager debounceManager, IRuntimeInfo runtimeInfo, Logger logger) { _healthChecks = healthChecks.ToArray(); + _serverSideNotificationService = serverSideNotificationService; _eventAggregator = eventAggregator; + _cacheManager = cacheManager; _logger = logger; - _healthCheckResults = cacheManager.GetCache(GetType()); - _pendingHealthChecks = new HashSet(); - _debounce = debounceManager.CreateDebouncer(ProcessHealthChecks, TimeSpan.FromSeconds(5)); + _healthCheckResults = _cacheManager.GetCache(GetType()); _startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray(); _scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray(); _eventDrivenHealthChecks = GetEventDrivenHealthChecks(); - _startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15); + _startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15); } public List Results() @@ -78,57 +77,31 @@ namespace NzbDrone.Core.HealthCheck .ToDictionary(g => g.Key, g => g.ToArray()); } - private void ProcessHealthChecks() + private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, bool performServerChecks = false) { - List healthChecks; + var results = healthChecks.Select(c => c.Check()) + .ToList(); - lock (_pendingHealthChecks) + if (performServerChecks) { - healthChecks = _pendingHealthChecks.ToList(); - _pendingHealthChecks.Clear(); + results.AddRange(_serverSideNotificationService.GetServerChecks()); } - _debounce.Pause(); - - try + foreach (var result in results) { - var results = healthChecks.Select(c => - { - _logger.Trace("Check health -> {0}", c.GetType().Name); - var result = c.Check(); - _logger.Trace("Check health <- {0}", c.GetType().Name); - - return result; - }) - .ToList(); - - foreach (var result in results) + if (result.Type == HealthCheckResult.Ok) { - if (result.Type == HealthCheckResult.Ok) - { - var previous = _healthCheckResults.Find(result.Source.Name); - - if (previous != null) - { - _eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod)); - } - - _healthCheckResults.Remove(result.Source.Name); - } - else - { - if (_healthCheckResults.Find(result.Source.Name) == null) - { - _eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod)); - } - - _healthCheckResults.Set(result.Source.Name, result); - } + _healthCheckResults.Remove(result.Source.Name); + } + else + { + if (_healthCheckResults.Find(result.Source.Name) == null) + { + _eventAggregator.PublishEvent(new HealthCheckFailedEvent(result, !_hasRunHealthChecksAfterGracePeriod)); + } + + _healthCheckResults.Set(result.Source.Name, result); } - } - finally - { - _debounce.Resume(); } _eventAggregator.PublishEvent(new HealthCheckCompleteEvent()); @@ -136,35 +109,24 @@ namespace NzbDrone.Core.HealthCheck public void Execute(CheckHealthCommand message) { - var healthChecks = message.Trigger == CommandTrigger.Manual ? _healthChecks : _scheduledHealthChecks; - - lock (_pendingHealthChecks) + if (message.Trigger == CommandTrigger.Manual) { - foreach (var healthCheck in healthChecks) - { - _pendingHealthChecks.Add(healthCheck); - } + PerformHealthCheck(_healthChecks, true); + } + else + { + PerformHealthCheck(_scheduledHealthChecks, true); } - - ProcessHealthChecks(); } public void HandleAsync(ApplicationStartedEvent message) { - lock (_pendingHealthChecks) - { - foreach (var healthCheck in _startupHealthChecks) - { - _pendingHealthChecks.Add(healthCheck); - } - } - - ProcessHealthChecks(); + PerformHealthCheck(_startupHealthChecks, true); } public void HandleAsync(IEvent message) { - if (message is HealthCheckCompleteEvent || message is ApplicationStartedEvent) + if (message is HealthCheckCompleteEvent) { return; } @@ -175,16 +137,7 @@ namespace NzbDrone.Core.HealthCheck { _isRunningHealthChecksAfterGracePeriod = true; - lock (_pendingHealthChecks) - { - foreach (var healthCheck in _startupHealthChecks) - { - _pendingHealthChecks.Add(healthCheck); - } - } - - // Call it directly so it's not debounced and any alerts can be sent. - ProcessHealthChecks(); + PerformHealthCheck(_startupHealthChecks); // Update after running health checks so new failure notifications aren't sent 2x. _hasRunHealthChecksAfterGracePeriod = true; @@ -200,7 +153,8 @@ namespace NzbDrone.Core.HealthCheck _isRunningHealthChecksAfterGracePeriod = false; } - if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out var checks)) + IEventDrivenHealthCheck[] checks; + if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks)) { return; } @@ -216,16 +170,11 @@ namespace NzbDrone.Core.HealthCheck if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed)) { filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); - continue; } } - lock (_pendingHealthChecks) - { - filteredChecks.ForEach(h => _pendingHealthChecks.Add(h)); - } - - _debounce.Execute(); + // TODO: Add debounce + PerformHealthCheck(filteredChecks.ToArray()); } } } diff --git a/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs b/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs index 51420e63b..dd742bdf6 100644 --- a/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs +++ b/src/NzbDrone.Core/HealthCheck/ServerSideNotificationService.cs @@ -9,43 +9,50 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Localization; namespace NzbDrone.Core.HealthCheck { - public class ServerSideNotificationService : HealthCheckBase + public interface IServerSideNotificationService + { + public List GetServerChecks(); + } + + public class ServerSideNotificationService : IServerSideNotificationService { private readonly IHttpClient _client; - private readonly IProwlarrCloudRequestBuilder _cloudRequestBuilder; private readonly IConfigFileProvider _configFileProvider; + private readonly IHttpRequestBuilderFactory _cloudRequestBuilder; private readonly Logger _logger; - private readonly ICached _cache; + private readonly ICached> _cache; - public ServerSideNotificationService(IHttpClient client, IProwlarrCloudRequestBuilder cloudRequestBuilder, IConfigFileProvider configFileProvider, ICacheManager cacheManager, ILocalizationService localizationService, Logger logger) - : base(localizationService) + public ServerSideNotificationService(IHttpClient client, + IConfigFileProvider configFileProvider, + IProwlarrCloudRequestBuilder cloudRequestBuilder, + ICacheManager cacheManager, + Logger logger) { _client = client; _configFileProvider = configFileProvider; - _cloudRequestBuilder = cloudRequestBuilder; + _cloudRequestBuilder = cloudRequestBuilder.Services; _logger = logger; - _cache = cacheManager.GetCache(GetType()); + _cache = cacheManager.GetCache>(GetType()); } - public override HealthCheck Check() + public List GetServerChecks() { return _cache.Get("ServerChecks", RetrieveServerChecks, TimeSpan.FromHours(2)); } - private HealthCheck RetrieveServerChecks() + private List RetrieveServerChecks() { if (BuildInfo.IsDebug) { - return new HealthCheck(GetType()); + return new List(); } - var request = _cloudRequestBuilder.Services.Create() + var request = _cloudRequestBuilder.Create() .Resource("/notification") .AddQueryParam("version", BuildInfo.Version) .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) @@ -56,22 +63,17 @@ namespace NzbDrone.Core.HealthCheck try { - _logger.Trace("Getting notifications"); - + _logger.Trace("Getting server side health notifications"); var response = _client.Execute(request); var result = Json.Deserialize>(response.Content); - - var checks = result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList(); - - // Only one health check is supported, services returns an ordered list, so use the first one - return checks.FirstOrDefault() ?? new HealthCheck(GetType()); + return result.Select(x => new HealthCheck(GetType(), x.Type, x.Message, x.WikiUrl)).ToList(); } catch (Exception ex) { - _logger.Error(ex, "Failed to retrieve notifications"); - - return new HealthCheck(GetType()); + _logger.Error(ex, "Failed to retrieve server notifications"); } + + return new List(); } } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e78e5d229..4352a1c67 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -3,7 +3,6 @@ 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; @@ -129,63 +128,56 @@ namespace NzbDrone.Core.History Successful = response?.StatusCode == HttpStatusCode.OK || (response is { Request: { SuppressHttpError: true, SuppressHttpErrorStatusCodes: not null } } && response.Request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)) }; - if (message.Query is MovieSearchCriteria movieSearchCriteria) + if (message.Query is MovieSearchCriteria) { - history.Data.Add("ImdbId", movieSearchCriteria.FullImdbId); - history.Data.Add("TmdbId", movieSearchCriteria.TmdbId?.ToString()); - history.Data.Add("TraktId", movieSearchCriteria.TraktId?.ToString()); - history.Data.Add("Year", movieSearchCriteria.Year?.ToString()); - history.Data.Add("Genre", movieSearchCriteria.Genre); + history.Data.Add("ImdbId", ((MovieSearchCriteria)message.Query).FullImdbId ?? string.Empty); + history.Data.Add("TmdbId", ((MovieSearchCriteria)message.Query).TmdbId?.ToString() ?? string.Empty); + history.Data.Add("TraktId", ((MovieSearchCriteria)message.Query).TraktId?.ToString() ?? string.Empty); + history.Data.Add("Year", ((MovieSearchCriteria)message.Query).Year?.ToString() ?? string.Empty); + history.Data.Add("Genre", ((MovieSearchCriteria)message.Query).Genre ?? string.Empty); } - if (message.Query is TvSearchCriteria tvSearchCriteria) + if (message.Query is TvSearchCriteria) { - history.Data.Add("ImdbId", tvSearchCriteria.FullImdbId); - history.Data.Add("TvdbId", tvSearchCriteria.TvdbId?.ToString()); - history.Data.Add("TmdbId", tvSearchCriteria.TmdbId?.ToString()); - history.Data.Add("TraktId", tvSearchCriteria.TraktId?.ToString()); - history.Data.Add("RId", tvSearchCriteria.RId?.ToString()); - history.Data.Add("TvMazeId", tvSearchCriteria.TvMazeId?.ToString()); - history.Data.Add("Season", tvSearchCriteria.Season?.ToString()); - history.Data.Add("Episode", tvSearchCriteria.Episode); - history.Data.Add("Year", tvSearchCriteria.Year?.ToString()); - history.Data.Add("Genre", tvSearchCriteria.Genre); + history.Data.Add("ImdbId", ((TvSearchCriteria)message.Query).FullImdbId ?? string.Empty); + history.Data.Add("TvdbId", ((TvSearchCriteria)message.Query).TvdbId?.ToString() ?? string.Empty); + history.Data.Add("TmdbId", ((TvSearchCriteria)message.Query).TmdbId?.ToString() ?? string.Empty); + history.Data.Add("TraktId", ((TvSearchCriteria)message.Query).TraktId?.ToString() ?? string.Empty); + history.Data.Add("RId", ((TvSearchCriteria)message.Query).RId?.ToString() ?? string.Empty); + history.Data.Add("TvMazeId", ((TvSearchCriteria)message.Query).TvMazeId?.ToString() ?? string.Empty); + history.Data.Add("Season", ((TvSearchCriteria)message.Query).Season?.ToString() ?? string.Empty); + history.Data.Add("Episode", ((TvSearchCriteria)message.Query).Episode ?? string.Empty); + history.Data.Add("Year", ((TvSearchCriteria)message.Query).Year?.ToString() ?? string.Empty); + history.Data.Add("Genre", ((TvSearchCriteria)message.Query).Genre ?? string.Empty); } - if (message.Query is MusicSearchCriteria musicSearchCriteria) + if (message.Query is MusicSearchCriteria) { - history.Data.Add("Artist", musicSearchCriteria.Artist); - history.Data.Add("Album", musicSearchCriteria.Album); - history.Data.Add("Track", musicSearchCriteria.Track); - history.Data.Add("Label", musicSearchCriteria.Label); - history.Data.Add("Year", musicSearchCriteria.Year?.ToString()); - history.Data.Add("Genre", musicSearchCriteria.Genre); + history.Data.Add("Artist", ((MusicSearchCriteria)message.Query).Artist ?? string.Empty); + history.Data.Add("Album", ((MusicSearchCriteria)message.Query).Album ?? string.Empty); + history.Data.Add("Track", ((MusicSearchCriteria)message.Query).Track ?? string.Empty); + history.Data.Add("Label", ((MusicSearchCriteria)message.Query).Label ?? string.Empty); + history.Data.Add("Year", ((MusicSearchCriteria)message.Query).Year?.ToString() ?? string.Empty); + history.Data.Add("Genre", ((MusicSearchCriteria)message.Query).Genre ?? string.Empty); } - if (message.Query is BookSearchCriteria bookSearchCriteria) + if (message.Query is BookSearchCriteria) { - history.Data.Add("Author", bookSearchCriteria.Author); - history.Data.Add("Title", bookSearchCriteria.Title); - history.Data.Add("Publisher", bookSearchCriteria.Publisher); - history.Data.Add("Year", bookSearchCriteria.Year?.ToString()); - history.Data.Add("Genre", bookSearchCriteria.Genre); + history.Data.Add("Author", ((BookSearchCriteria)message.Query).Author ?? string.Empty); + history.Data.Add("BookTitle", ((BookSearchCriteria)message.Query).Title ?? string.Empty); + history.Data.Add("Publisher", ((BookSearchCriteria)message.Query).Publisher ?? string.Empty); + history.Data.Add("Year", ((BookSearchCriteria)message.Query).Year?.ToString() ?? string.Empty); + history.Data.Add("Genre", ((BookSearchCriteria)message.Query).Genre ?? string.Empty); } - history.Data.Add("Limit", message.Query.Limit?.ToString()); - history.Data.Add("Offset", message.Query.Offset?.ToString()); - - // Clean empty data - history.Data = history.Data.Where(d => d.Value != null).ToDictionary(x => x.Key, x => x.Value); - - history.Data.Add("ElapsedTime", message.QueryResult.Cached ? "0" : message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty); + history.Data.Add("ElapsedTime", message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty); history.Data.Add("Query", message.Query.SearchTerm ?? string.Empty); history.Data.Add("QueryType", message.Query.SearchType ?? string.Empty); - history.Data.Add("Categories", string.Join(",", message.Query.Categories ?? Array.Empty())); + history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty); history.Data.Add("Source", message.Query.Source ?? string.Empty); history.Data.Add("Host", message.Query.Host ?? string.Empty); history.Data.Add("QueryResults", message.QueryResult.Releases?.Count.ToString() ?? string.Empty); history.Data.Add("Url", message.QueryResult.Response?.Request.Url.FullUri ?? string.Empty); - history.Data.Add("Cached", message.QueryResult.Cached ? "1" : "0"); _historyRepository.Insert(history); } @@ -203,30 +195,9 @@ namespace NzbDrone.Core.History history.Data.Add("Source", message.Source ?? string.Empty); 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("Title", message.Title); 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); } @@ -240,7 +211,7 @@ namespace NzbDrone.Core.History Successful = message.Successful }; - history.Data.Add("ElapsedTime", message.ElapsedTime.ToString()); + history.Data.Add("ElapsedTime", message.Time.ToString()); _historyRepository.Insert(history); } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs index 0f525a278..8b36317ec 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs @@ -14,11 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using var mapper = _database.OpenConnection(); - mapper.Execute(@"DELETE FROM ""Users"" - WHERE ""Id"" NOT IN ( - SELECT ""Id"" FROM ""Users"" - LIMIT 1)"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ""Users"" + WHERE ""Id"" NOT IN ( + SELECT ""Id"" FROM ""Users"" + LIMIT 1)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedApplicationStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedApplicationStatus.cs index 3c5d91ca1..86e4cd2a5 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedApplicationStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedApplicationStatus.cs @@ -14,13 +14,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using var mapper = _database.OpenConnection(); + var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""ApplicationStatus"" - WHERE ""Id"" IN ( - SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus"" - LEFT OUTER JOIN ""Applications"" - ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id"" - WHERE ""Applications"".""Id"" IS NULL)"); + WHERE ""Id"" IN ( + SELECT ""ApplicationStatus"".""Id"" FROM ""ApplicationStatus"" + LEFT OUTER JOIN ""Applications"" + ON ""ApplicationStatus"".""ProviderId"" = ""Applications"".""Id"" + WHERE ""Applications"".""Id"" IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs index fe1adfbd6..5a6af9fd8 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs @@ -14,13 +14,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using var mapper = _database.OpenConnection(); + var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""DownloadClientStatus"" - WHERE ""Id"" IN ( - SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus"" - LEFT OUTER JOIN ""DownloadClients"" - ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id"" - WHERE ""DownloadClients"".""Id"" IS NULL)"); + WHERE ""Id"" IN ( + SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus"" + LEFT OUTER JOIN ""DownloadClients"" + ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id"" + WHERE ""DownloadClients"".""Id"" IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs index 571304073..e6d8d789d 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs @@ -19,13 +19,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private void CleanupOrphanedByIndexer() { - using var mapper = _database.OpenConnection(); - mapper.Execute(@"DELETE FROM ""History"" - WHERE ""Id"" IN ( - SELECT ""History"".""Id"" FROM ""History"" - LEFT OUTER JOIN ""Indexers"" - ON ""History"".""IndexerId"" = ""Indexers"".""Id"" - WHERE ""Indexers"".""Id"" IS NULL)"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ""History"" + WHERE ""Id"" IN ( + SELECT ""History"".""Id"" FROM ""History"" + LEFT OUTER JOIN ""Indexers"" + ON ""History"".""IndexerId"" = ""Indexers"".""Id"" + WHERE ""Indexers"".""Id"" IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs index 9486641b5..059f059e4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs @@ -14,13 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using var mapper = _database.OpenConnection(); - mapper.Execute(@"DELETE FROM ""IndexerStatus"" - WHERE ""Id"" IN ( - SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus"" - LEFT OUTER JOIN ""Indexers"" - ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id"" - WHERE ""Indexers"".""Id"" IS NULL)"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ""IndexerStatus"" + WHERE ""Id"" IN ( + SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus"" + LEFT OUTER JOIN ""Indexers"" + ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id"" + WHERE ""Indexers"".""Id"" IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs deleted file mode 100644 index cfc3e1f63..000000000 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedNotificationStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Dapper; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Housekeeping.Housekeepers -{ - public class CleanupOrphanedNotificationStatus : IHousekeepingTask - { - private readonly IMainDatabase _database; - - public CleanupOrphanedNotificationStatus(IMainDatabase database) - { - _database = database; - } - - public void Clean() - { - using var mapper = _database.OpenConnection(); - - mapper.Execute(@"DELETE FROM ""NotificationStatus"" - WHERE ""Id"" IN ( - SELECT ""NotificationStatus"".""Id"" FROM ""NotificationStatus"" - LEFT OUTER JOIN ""Notifications"" - ON ""NotificationStatus"".""ProviderId"" = ""Notifications"".""Id"" - WHERE ""Notifications"".""Id"" IS NULL)"); - } - } -} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 1ad3f91f1..1df062918 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -17,21 +17,23 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using var mapper = _database.OpenConnection(); - var usedTags = new[] { "Notifications", "IndexerProxies", "Indexers", "Applications" } - .SelectMany(v => GetUsedTags(v, mapper)) - .Distinct() - .ToArray(); - - if (usedTags.Length > 0) + using (var mapper = _database.OpenConnection()) { - var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray()); + var usedTags = new[] { "Notifications", "IndexerProxies", "Indexers", "Applications" } + .SelectMany(v => GetUsedTags(v, mapper)) + .Distinct() + .ToArray(); - mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})"); - } - else - { - mapper.Execute("DELETE FROM \"Tags\""); + if (usedTags.Length > 0) + { + var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray()); + + mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})"); + } + else + { + mapper.Execute($"DELETE FROM \"Tags\""); + } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs deleted file mode 100644 index 10af6ab42..000000000 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureNotificationStatusTimes.cs +++ /dev/null @@ -1,12 +0,0 @@ -using NzbDrone.Core.Notifications; - -namespace NzbDrone.Core.Housekeeping.Housekeepers -{ - public class FixFutureNotificationStatusTimes : FixFutureProviderStatusTimes, IHousekeepingTask - { - public FixFutureNotificationStatusTimes(INotificationStatusRepository notificationStatusRepository) - : base(notificationStatusRepository) - { - } - } -} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs index 62704c023..6b45ed0c4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs @@ -24,11 +24,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers _logger.Debug("Not running scheduled task last execution cleanup during debug"); } - using var mapper = _database.OpenConnection(); - mapper.Execute(@"UPDATE ""ScheduledTasks"" - SET ""LastExecution"" = @time - WHERE ""LastExecution"" > @time", - new { time = DateTime.UtcNow }); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"UPDATE ""ScheduledTasks"" + SET ""LastExecution"" = @time + WHERE ""LastExecution"" > @time", + new { time = DateTime.UtcNow }); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs index 5763a563e..a719652af 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/TrimLogDatabase.cs @@ -1,26 +1,18 @@ -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Instrumentation; +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, IConfigFileProvider configFileProvider) + public TrimLogDatabase(ILogRepository logRepo) { _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 24b5aa67f..a56bcf2e6 100644 --- a/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs +++ b/src/NzbDrone.Core/Http/HttpProxySettingsProvider.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; using System.Net; -using NetTools; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration; @@ -54,15 +52,7 @@ 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) || 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)); + return proxy.IsBypassed((Uri)url); } } } diff --git a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs index 5107bf151..c3edb880e 100644 --- a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs +++ b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverr.cs @@ -102,15 +102,9 @@ 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 ? 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 - }; + var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null; if (request.Method == HttpMethod.Get) { @@ -119,7 +113,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr Cmd = "request.get", Url = url, MaxTimeout = maxTimeout, - Proxy = requestProxy + Proxy = new FlareSolverrProxy + { + Url = proxyUrl?.AbsoluteUri + } }; } else if (request.Method == HttpMethod.Post) @@ -142,7 +139,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr ContentLength = null }, MaxTimeout = maxTimeout, - Proxy = requestProxy + Proxy = new FlareSolverrProxy + { + Url = proxyUrl?.AbsoluteUri + } }; } else if (contentTypeType.Contains("multipart/form-data") @@ -169,7 +169,6 @@ 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); @@ -190,16 +189,16 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr if (response.StatusCode != HttpStatusCode.OK) { - _logger.Error("Proxy validation failed: {0}", response.StatusCode); - failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary { { "statusCode", response.StatusCode } }))); + _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); + failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode))); } var result = JsonConvert.DeserializeObject(response.Content); } catch (Exception ex) { - _logger.Error(ex, "Proxy validation failed"); - failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } }))); + _logger.Error(ex, "Proxy Health Check failed"); + failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckFailedToTestMessage"), request.Url.Host))); } return new ValidationResult(failures); @@ -207,13 +206,17 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr private Uri GetProxyUri(HttpProxySettings proxySettings) { - return proxySettings.Type switch + switch (proxySettings.Type) { - 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 - }; + 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; + } } private class FlareSolverrRequest @@ -244,8 +247,6 @@ 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/FlareSolverr/FlareSolverrSettings.cs b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverrSettings.cs index 87a877ee1..9ea860ba3 100644 --- a/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverrSettings.cs +++ b/src/NzbDrone.Core/IndexerProxies/FlareSolverr/FlareSolverrSettings.cs @@ -1,4 +1,5 @@ using FluentValidation; +using NLog.Config; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; diff --git a/src/NzbDrone.Core/IndexerProxies/Http/Http.cs b/src/NzbDrone.Core/IndexerProxies/Http/Http.cs index 51758b6ac..8994f2919 100644 --- a/src/NzbDrone.Core/IndexerProxies/Http/Http.cs +++ b/src/NzbDrone.Core/IndexerProxies/Http/Http.cs @@ -1,8 +1,11 @@ +using System.Net; using NLog; using NzbDrone.Common.Cloud; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Localization; +using NzbDrone.Core.Notifications.Prowl; namespace NzbDrone.Core.IndexerProxies.Http { diff --git a/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs index d7c9acc23..431ff53a9 100644 --- a/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs +++ b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using FluentValidation.Results; using NLog; @@ -31,8 +32,8 @@ namespace NzbDrone.Core.IndexerProxies var failures = new List(); var request = PreRequest(_cloudRequestBuilder.Create() - .Resource("/ping") - .Build()); + .Resource("/ping") + .Build()); try { @@ -41,14 +42,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 validation failed: {0}", response.StatusCode); - failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationBadRequest", new Dictionary { { "statusCode", response.StatusCode } }))); + _logger.Error("Proxy Health Check failed: {0}", response.StatusCode); + failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy. StatusCode: {0}", response.StatusCode))); } } catch (Exception ex) { - _logger.Error(ex, "Proxy validation failed"); - failures.Add(new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("ProxyValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } }))); + _logger.Error(ex, "Proxy Health Check failed"); + failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message))); } return new ValidationResult(failures); diff --git a/src/NzbDrone.Core/IndexerProxies/IndexerProxyFactory.cs b/src/NzbDrone.Core/IndexerProxies/IndexerProxyFactory.cs index a103d676d..c21d9e2f4 100644 --- a/src/NzbDrone.Core/IndexerProxies/IndexerProxyFactory.cs +++ b/src/NzbDrone.Core/IndexerProxies/IndexerProxyFactory.cs @@ -17,10 +17,5 @@ namespace NzbDrone.Core.IndexerProxies : base(providerRepository, providers, container, eventAggregator, logger) { } - - protected override List Active() - { - return All().Where(c => c.Enable && c.Settings.Validate().IsValid).ToList(); - } } } diff --git a/src/NzbDrone.Core/IndexerProxies/IndexerProxyService.cs b/src/NzbDrone.Core/IndexerProxies/IndexerProxyService.cs index f27b316b5..5a86d1fb8 100644 --- a/src/NzbDrone.Core/IndexerProxies/IndexerProxyService.cs +++ b/src/NzbDrone.Core/IndexerProxies/IndexerProxyService.cs @@ -1,4 +1,7 @@ +using System; using NLog; +using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.IndexerProxies { diff --git a/src/NzbDrone.Core/IndexerProxies/Socks4/Socks4.cs b/src/NzbDrone.Core/IndexerProxies/Socks4/Socks4.cs index 3db9f6860..554d8a78b 100644 --- a/src/NzbDrone.Core/IndexerProxies/Socks4/Socks4.cs +++ b/src/NzbDrone.Core/IndexerProxies/Socks4/Socks4.cs @@ -1,6 +1,8 @@ using System; +using System.Net; using NLog; using NzbDrone.Common.Cloud; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Localization; diff --git a/src/NzbDrone.Core/IndexerProxies/Socks5/Socks5.cs b/src/NzbDrone.Core/IndexerProxies/Socks5/Socks5.cs index 725b49e70..3a7ed0dbe 100644 --- a/src/NzbDrone.Core/IndexerProxies/Socks5/Socks5.cs +++ b/src/NzbDrone.Core/IndexerProxies/Socks5/Socks5.cs @@ -1,6 +1,8 @@ using System; +using System.Net; using NLog; using NzbDrone.Common.Cloud; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Localization; diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 48e7586c5..339abff86 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -17,10 +17,6 @@ namespace NzbDrone.Core.IndexerSearch.Definitions public string SearchType { get; set; } public int? Limit { get; set; } public int? Offset { get; set; } - public int? MinAge { get; set; } - public int? MaxAge { get; set; } - public long? MinSize { get; set; } - public long? MaxSize { get; set; } public string Source { get; set; } public string Host { get; set; } diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs index d4c13c863..1e4c35e5b 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/TvSearchCriteria.cs @@ -31,7 +31,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions !IsIdSearch; public override bool IsIdSearch => + Episode.IsNotNullOrWhiteSpace() || ImdbId.IsNotNullOrWhiteSpace() || + Season.HasValue || TvdbId.HasValue || RId.HasValue || TraktId.HasValue || @@ -43,20 +45,14 @@ namespace NzbDrone.Core.IndexerSearch.Definitions { get { - var searchQueryTerm = "Term: []"; + var searchQueryTerm = $"Term: []"; var searchEpisodeTerm = $" for Season / Episode:[{EpisodeSearchString}]"; if (SearchTerm.IsNotNullOrWhiteSpace()) { searchQueryTerm = $"Term: [{SearchTerm}]"; } - if (!ImdbId.IsNotNullOrWhiteSpace() && - !TvdbId.HasValue && - !RId.HasValue && - !TraktId.HasValue && - !TvMazeId.HasValue && - !TmdbId.HasValue && - !DoubanId.HasValue) + if (!ImdbId.IsNotNullOrWhiteSpace() && !TvdbId.HasValue && !RId.HasValue && !TraktId.HasValue) { return $"{searchQueryTerm}{searchEpisodeTerm}"; } @@ -84,21 +80,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions builder.Append($" TraktId:[{TraktId}]"); } - if (TvMazeId.HasValue) - { - builder.Append($" TvMazeId:[{TvMazeId}]"); - } - if (TmdbId.HasValue) { builder.Append($" TmdbId:[{TmdbId}]"); } - if (DoubanId.HasValue) - { - builder.Append($" DoubanId:[{DoubanId}]"); - } - builder = builder.Append(searchEpisodeTerm); return builder.ToString().Trim(); } @@ -106,29 +92,29 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private string GetEpisodeSearchString() { - if (Season is null or 0) + if (Season == null || Season == 0) { return string.Empty; } string episodeString; - if (DateTime.TryParseExact($"{Season} {Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) + if (DateTime.TryParseExact(string.Format("{0} {1}", Season, Episode), "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate)) { - episodeString = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture); + episodeString = showDate.ToString("yyyy.MM.dd"); } else if (Episode.IsNullOrWhiteSpace()) { - episodeString = $"S{Season:00}"; + episodeString = string.Format("S{0:00}", Season); } else { try { - episodeString = $"S{Season:00}E{ParseUtil.CoerceInt(Episode):00}"; + episodeString = string.Format("S{0:00}E{1:00}", Season, ParseUtil.CoerceInt(Episode)); } catch (FormatException) { - episodeString = $"S{Season:00}E{Episode}"; + episodeString = string.Format("S{0:00}E{1}", Season, Episode); } } diff --git a/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs b/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs index 442b7ba00..5d5e82fb3 100644 --- a/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs +++ b/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs @@ -1,14 +1,13 @@ using System.Text.RegularExpressions; -using NzbDrone.Common.Extensions; namespace NzbDrone.Core.IndexerSearch { public class NewznabRequest { - private static readonly Regex TvRegex = new (@"\{((?:imdbid\:)(?[^{]+)|(?:rid\:)(?[^{]+)|(?:tvdbid\:)(?[^{]+)|(?:tmdbid\:)(?[^{]+)|(?:tvmazeid\:)(?[^{]+)|(?:doubanid\:)(?[^{]+)|(?:season\:)(?[^{]+)|(?:episode\:)(?[^{]+)|(?:year\:)(?[^{]+)|(?:genre\:)(?[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex MovieRegex = new (@"\{((?:imdbid\:)(?[^{]+)|(?:doubanid\:)(?[^{]+)|(?:tmdbid\:)(?[^{]+)|(?:traktid\:)(?[^{]+)|(?:year\:)(?[^{]+)|(?:genre\:)(?[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex MusicRegex = new (@"\{((?:artist\:)(?[^{]+)|(?:album\:)(?[^{]+)|(?:track\:)(?[^{]+)|(?:label\:)(?