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/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f70e2c23e..30d988d53 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 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..24fc04567 100644 --- a/.github/label-actions.yml +++ b/.github/label-actions.yml @@ -4,7 +4,8 @@ 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' diff --git a/.github/labeler.yml b/.github/labeler.yml index 74160b634..21aacef8c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,31 +1,19 @@ 'Area: API': - - changed-files: - - any-glob-to-any-file: - - src/Prowlarr.Api.V1/**/* + - src/Prowlarr.Api.V1/**/* 'Area: Db-migration': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Datastore/Migration/* + - src/NzbDrone.Core/Datastore/Migration/* 'Area: Download Clients': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Download/Clients/**/* + - src/NzbDrone.Core/Download/Clients/**/* 'Area: Indexer': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Indexers/**/* + - src/NzbDrone.Core/Indexers/**/* 'Area: Notifications': - - changed-files: - - any-glob-to-any-file: - - src/NzbDrone.Core/Notifications/**/* + - src/NzbDrone.Core/Notifications/**/* 'Area: UI': - - changed-files: - - any-glob-to-any-file: - - frontend/**/* - - package.json - - yarn.lock \ No newline at end of file + - frontend/**/* + - package.json + - yarn.lock 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 index ab2292824..857cfb4a7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v4 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..2383f7f05 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.7.2' 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' @@ -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 @@ -1126,7 +1003,7 @@ stages: git add . if git status | grep -q modified then - git commit -am 'Automated API Docs update' + git commit -am 'Automated API Docs update [skip ci]' git push -f --set-upstream origin api-docs curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' else @@ -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/.eslintrc.js b/frontend/.eslintrc.js index 56eaaeaab..c312414a2 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -26,8 +26,7 @@ module.exports = { globals: { expect: false, chai: false, - sinon: false, - JSX: true + sinon: false }, parserOptions: { @@ -357,16 +356,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 +373,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/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..7c1dcba83 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: { @@ -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]' } } }, 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/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/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..d511963fc 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,6 +1,4 @@ -import Column from 'Components/Table/Column'; import SortDirection from 'Helpers/Props/SortDirection'; -import { FilterBuilderProp, PropertyFilter } from './AppState'; export interface Error { responseJSON: { @@ -19,19 +17,7 @@ export interface AppSectionSaveState { } export interface PagedAppSectionState { - page: number; pageSize: number; - totalPages: number; - totalRecords?: number; -} -export interface TableAppSectionState { - columns: Column[]; -} - -export interface AppSectionFilterState { - selectedFilterKey: string; - filters: PropertyFilter[]; - filterBuilderProps: FilterBuilderProp[]; } export interface AppSectionSchemaState { @@ -47,7 +33,6 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; - pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 0f0e82c0d..6e868cce4 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,13 +1,5 @@ -import CommandAppState from './CommandAppState'; -import HistoryAppState from './HistoryAppState'; -import IndexerAppState, { - IndexerHistoryAppState, - IndexerIndexAppState, - IndexerStatusAppState, -} from './IndexerAppState'; -import IndexerStatsAppState from './IndexerStatsAppState'; +import IndexerAppState from './IndexerAppState'; import SettingsAppState from './SettingsAppState'; -import SystemAppState from './SystemAppState'; import TagsAppState from './TagsAppState'; interface FilterBuilderPropOption { @@ -42,29 +34,9 @@ export interface CustomFilter { filers: PropertyFilter[]; } -export interface AppSectionState { - isConnected: boolean; - isReconnecting: boolean; - version: string; - prevVersion?: string; - dimensions: { - isSmallScreen: boolean; - width: number; - height: number; - }; -} - interface AppState { - app: AppSectionState; - commands: CommandAppState; - history: HistoryAppState; - indexerHistory: IndexerHistoryAppState; - indexerIndex: IndexerIndexAppState; - indexerStats: IndexerStatsAppState; - indexerStatus: IndexerStatusAppState; indexers: IndexerAppState; settings: SettingsAppState; - system: SystemAppState; tags: TagsAppState; } 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 index 4c0145d0d..ad7e778e6 100644 --- a/frontend/src/App/State/IndexerAppState.ts +++ b/frontend/src/App/State/IndexerAppState.ts @@ -1,42 +1,12 @@ -import Column from 'Components/Table/Column'; -import SortDirection from 'Helpers/Props/SortDirection'; -import Indexer, { IndexerStatus } from 'Indexer/Indexer'; -import History from 'typings/History'; +import Indexer from 'typings/Indexer'; 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; + AppSectionSaveState {} 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 index 33c6c936d..bded59754 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -1,40 +1,19 @@ 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 {} +import { UiSettings } from 'typings/UiSettings'; export interface ApplicationAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} + AppSectionSaveState {} export interface DownloadClientAppState extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState { - isTestingAll: boolean; -} - -export interface GeneralAppState - extends AppSectionItemState, - AppSectionSaveState {} - -export interface IndexerCategoryAppState - extends AppSectionState, AppSectionDeleteState, AppSectionSaveState {} @@ -42,16 +21,13 @@ export interface NotificationAppState extends AppSectionState, AppSectionDeleteState {} -export type UiSettingsAppState = AppSectionItemState; +export type UiSettingsAppState = AppSectionState; interface SettingsAppState { - appProfiles: AppProfileAppState; applications: ApplicationAppState; downloadClients: DownloadClientAppState; - general: GeneralAppState; - indexerCategories: IndexerCategoryAppState; notifications: NotificationAppState; - ui: UiSettingsAppState; + uiSettings: 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 index 53a0d847f..d1f1d5a2f 100644 --- a/frontend/src/App/State/TagsAppState.ts +++ b/frontend/src/App/State/TagsAppState.ts @@ -1,28 +1,12 @@ 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; -} +interface TagsAppState extends AppSectionState, AppSectionDeleteState {} 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..aac01a6b5 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 ( -
+
@@ -36,7 +35,6 @@ class DescriptionListItem extends Component { } DescriptionListItem.propTypes = { - className: PropTypes.string, titleClassName: PropTypes.string, descriptionClassName: PropTypes.string, title: PropTypes.string, diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 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..fd2ff8afa 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -20,12 +20,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 } ]; 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/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..1aef10c30 100644 --- a/frontend/src/Components/Form/AppProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/AppProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import translate from 'Utilities/String/translate'; 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 +24,16 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - get value() { - return translate('NoChange'); - }, - isDisabled: true + value: translate('NoChange'), + 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 index 9cf7a429a..162c79885 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -3,8 +3,7 @@ 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 sortByName from 'Utilities/Array/sortByName'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -22,17 +21,16 @@ function createMapStateToProps() { const values = items .filter((downloadClient) => downloadClient.protocol === protocolFilter) - .sort(sortByProp('name')) + .sort(sortByName) .map((downloadClient) => ({ key: downloadClient.id, - value: downloadClient.name, - hint: `(${downloadClient.id})` + value: downloadClient.name })); if (includeAny) { values.unshift({ key: 0, - value: `(${translate('Any')})` + value: '(Any)' }); } diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js index 79b1c999c..cc4215025 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/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/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} > 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..28c12df12 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -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..8c2122c65 100644 --- a/frontend/src/Components/Table/Column.ts +++ b/frontend/src/Components/Table/Column.ts @@ -1,12 +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; + label: string | React.ReactNode; columnLabel?: string; isSortable?: boolean; isVisible: boolean; diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js index 8afbf9ea0..befc8219a 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} ); }) 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 { 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..b576a988c 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') } }; 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/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/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/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/icons.js b/frontend/src/Helpers/Props/icons.js index 773748996..589add5a8 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, @@ -76,7 +75,6 @@ import { faListCheck as fasListCheck, faLocationArrow as fasLocationArrow, faLock as fasLock, - faMagnet as fasMagnet, faMedkit as fasMedkit, faMinus as fasMinus, faMusic as fasMusic, @@ -142,7 +140,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,7 +181,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; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index f9cd58e6d..d26d08616 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'; @@ -9,7 +10,6 @@ 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 +26,7 @@ export const TAG_SELECT = 'tagSelect'; export const all = [ AUTO_COMPLETE, APP_PROFILE_SELECT, + AVAILABILITY_SELECT, CAPTCHA, CARDIGANNCAPTCHA, CHECK, @@ -34,7 +35,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..e0ae06eb1 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') { @@ -25,10 +21,7 @@ function HistoryDetails(props) { limit, offset, source, - host, - url, - elapsedTime, - cached + url } = data; return ( @@ -93,15 +86,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -135,19 +101,10 @@ function HistoryDetails(props) { if (eventType === 'releaseGrabbed') { const { source, - host, grabTitle, - url, - publishedDate, - infoUrl, - downloadClient, - downloadClientName, - elapsedTime, - grabMethod + url } = data; - const downloadClientNameInfo = downloadClientName ?? downloadClient; - return ( { @@ -168,15 +125,6 @@ function HistoryDetails(props) { null } - { - data ? - : - null - } - { data ? {infoUrl}} - /> : - null - } - - { - publishedDate ? - : - null - } - - { - downloadClientNameInfo ? - : - null - } - { data ? : null } - - { - elapsedTime ? - : - null - } - - { - grabMethod ? - : - null - } - - { - date ? - : - null - } ); } if (eventType === 'indexerAuth') { - const { elapsedTime } = data; - return ( : null } - - { - elapsedTime ? - : - null - } - - { - date ? - : - null - } ); } @@ -297,15 +171,6 @@ function HistoryDetails(props) { title={translate('Name')} data={data.query} /> - - { - date ? - : - null - } ); } @@ -313,7 +178,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 + + } +
+ ); + } +} + +HistoryRowParameter.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.string.isRequired +}; + +export default HistoryRowParameter; diff --git a/frontend/src/History/HistoryRowParameter.tsx b/frontend/src/History/HistoryRowParameter.tsx deleted file mode 100644 index ad83d5d77..000000000 --- a/frontend/src/History/HistoryRowParameter.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import Link from 'Components/Link/Link'; -import { HistoryQueryType } from 'typings/History'; -import styles from './HistoryRowParameter.css'; - -interface HistoryRowParameterProps { - title: string; - value: string; - queryType: HistoryQueryType; -} - -function HistoryRowParameter(props: HistoryRowParameterProps) { - const { title, value, queryType } = props; - - const type = title.toLowerCase(); - - let link = null; - - if (type === 'imdb') { - link = {value}; - } else if (type === 'tmdb') { - link = ( - - {value} - - ); - } else if (type === 'tvdb') { - link = ( - - {value} - - ); - } else if (type === 'tvmaze') { - link = {value}; - } - - return ( -
-
- {title} -
- -
{link ? link : value}
-
- ); -} - -export default HistoryRowParameter; diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js new file mode 100644 index 000000000..4c4db24b9 --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModal.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; +import styles from './AddIndexerModal.css'; + +function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) { + return ( + + + + ); +} + +AddIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onSelectIndexer: PropTypes.func.isRequired +}; + +export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx deleted file mode 100644 index be22eec57..000000000 --- a/frontend/src/Indexer/Add/AddIndexerModal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import { clearIndexerSchema } from 'Store/Actions/indexerActions'; -import AddIndexerModalContent from './AddIndexerModalContent'; -import styles from './AddIndexerModal.css'; - -interface AddIndexerModalProps { - isOpen: boolean; - onSelectIndexer(): void; - onModalClose(): void; -} - -function AddIndexerModal({ - isOpen, - onSelectIndexer, - onModalClose, - ...otherProps -}: AddIndexerModalProps) { - const dispatch = useDispatch(); - - const onModalClosePress = useCallback(() => { - dispatch(clearIndexerSchema()); - onModalClose(); - }, [dispatch, onModalClose]); - - return ( - - - - ); -} - -export default AddIndexerModal; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css index e824c5475..5a92b40cb 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css @@ -19,16 +19,10 @@ margin-bottom: 16px; } -.notice { - composes: alert from '~Components/Alert.css'; - - margin-bottom: 20px; -} - .alert { composes: alert from '~Components/Alert.css'; - text-align: center; + margin-bottom: 20px; } .scroller { @@ -46,6 +40,7 @@ flex: 1; flex-direction: column; margin-right: 12px; + max-width: 50%; } .filterContainer:last-child { @@ -58,22 +53,17 @@ } @media only screen and (max-width: $breakpointSmall) { - .filterInput { - margin-bottom: 5px; - } - .alert { display: none; } .filterRow { - display: block; - margin-bottom: 10px; + flex-direction: column; } .filterContainer { margin-right: 0; - margin-bottom: 5px; + margin-bottom: 12px; } .scroller { @@ -83,12 +73,6 @@ } } -@media only screen and (min-width: $breakpointSmall) { - .filterContainer { - max-width: 50%; - } -} - .modalFooter { composes: modalFooter from '~Components/Modal/ModalFooter.css'; @@ -104,8 +88,4 @@ flex-direction: column; gap: 10px; } - - .available { - display: none; - } } diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts index 5978832e4..cbedc72a4 100644 --- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts @@ -10,7 +10,6 @@ interface CssExports { 'indexers': string; 'modalBody': string; 'modalFooter': string; - 'notice': string; 'scroller': string; } export const cssExports: CssExports; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js new file mode 100644 index 000000000..4617664ad --- /dev/null +++ b/frontend/src/Indexer/Add/AddIndexerModalContent.js @@ -0,0 +1,311 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Alert from 'Components/Alert'; +import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput'; +import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector'; +import TextInput from 'Components/Form/TextInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import Scroller from 'Components/Scroller/Scroller'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { kinds, scrollDirections } from 'Helpers/Props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; +import SelectIndexerRowConnector from './SelectIndexerRowConnector'; +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 + } +]; + +const protocols = [ + { + key: 'torrent', + value: 'torrent' + }, + { + key: 'usenet', + value: 'nzb' + } +]; + +const privacyLevels = [ + { + key: 'private', + value: translate('Private') + }, + { + key: 'semiPrivate', + value: translate('SemiPrivate') + }, + { + key: 'public', + value: translate('Public') + } +]; + +class AddIndexerModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + filter: '', + filterProtocols: [], + filterLanguages: [], + filterPrivacyLevels: [], + filterCategories: [] + }; + } + + // + // Listeners + + onFilterChange = ({ value }) => { + this.setState({ filter: value }); + }; + + // + // Render + + render() { + const { + indexers, + onIndexerSelect, + sortKey, + sortDirection, + isFetching, + isPopulated, + error, + onSortPress, + onModalClose + } = this.props; + + const languages = Array.from(new Set(indexers.map(({ language }) => language))) + .sort((a, b) => a.localeCompare(b)) + .map((language) => ({ key: language, value: language })); + + const filteredIndexers = indexers.filter((indexer) => { + const { + filter, + filterProtocols, + filterLanguages, + filterPrivacyLevels, + filterCategories + } = this.state; + + if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) { + return false; + } + + if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) { + return false; + } + + if (filterLanguages.length && !filterLanguages.includes(indexer.language)) { + return false; + } + + if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) { + return false; + } + + if (filterCategories.length) { + const { categories = [] } = indexer.capabilities || {}; + const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)]; + const flatCategories = categories + .filter((item) => item.id < 100000) + .flatMap(flat); + + if (!filterCategories.every((item) => flatCategories.includes(item))) { + return false; + } + } + + return true; + }); + + const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers')); + + return ( + + + {translate('AddIndexer')} + + + + + +
+
+ + this.setState({ filterProtocols: value })} + /> +
+ +
+ + this.setState({ filterLanguages: value })} + /> +
+ +
+ + this.setState({ filterPrivacyLevels: value })} + /> +
+ +
+ + this.setState({ filterCategories: value })} + /> +
+
+ + +
+ {translate('ProwlarrSupportsAnyIndexer')} +
+
+ + + { + isFetching ? : null + } + { + error ? {errorMessage} : null + } + { + isPopulated && !!indexers.length ? + + + { + filteredIndexers.map((indexer) => ( + + )) + } + +
: + null + } + { + isPopulated && !!indexers.length && !filteredIndexers.length ? + + {translate('NoIndexersFound')} + : + null + } +
+
+ + +
+ { + isPopulated ? + translate('CountIndexersAvailable', [filteredIndexers.length]) : + null + } +
+ +
+ +
+
+
+ ); + } +} + +AddIndexerModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + indexers: PropTypes.arrayOf(PropTypes.object).isRequired, + onIndexerSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModalContent; diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx 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..4a2ec4c0e 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js @@ -61,7 +61,7 @@ function EditIndexerModalContent(props) { return ( - {id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })} + {`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`} @@ -97,7 +97,7 @@ function EditIndexerModalContent(props) { @@ -144,7 +144,6 @@ function EditIndexerModalContent(props) { }) : null } - diff --git a/frontend/src/Indexer/Index/IndexerIndex.tsx b/frontend/src/Indexer/Index/IndexerIndex.tsx index e20e269f8..dcb7b8c9e 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,37 +112,37 @@ 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] ); @@ -184,7 +155,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { }; } - const characters = items.reduce((acc: Record, item) => { + const characters = items.reduce((acc, item) => { let char = item.sortName.charAt(0); if (!isNaN(Number(char))) { @@ -306,8 +277,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => { { jumpToCharacter={jumpToCharacter} isSelectMode={isSelectMode} isSmallScreen={isSmallScreen} - onCloneIndexerPress={onCloneIndexerPress} /> 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..0e27902fe 100644 --- a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx @@ -7,7 +7,6 @@ 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 createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector'; import translate from 'Utilities/String/translate'; @@ -21,15 +20,15 @@ interface DeleteIndexerModalContentProps { function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) { const { indexerIds, onModalClose } = props; - const allIndexers: Indexer[] = useSelector(createAllIndexersSelector()); + const allIndexers = useSelector(createAllIndexersSelector()); const dispatch = useDispatch(); - const indexers = useMemo((): Indexer[] => { - const indexerList = indexerIds.map((id) => { + const selectedIndexers = useMemo(() => { + const indexers = indexerIds.map((id) => { return allIndexers.find((s) => s.id === id); - }) as Indexer[]; + }); - return orderBy(indexerList, ['sortName']); + return orderBy(indexers, ['sortName']); }, [indexerIds, allIndexers]); const onDeleteIndexerConfirmed = useCallback(() => { @@ -48,13 +47,13 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
- {translate('DeleteSelectedIndexersMessageText', { - count: indexers.length, - })} + {translate('DeleteSelectedIndexersMessageText', [ + selectedIndexers.length, + ])}
    - {indexers.map((s) => { + {selectedIndexers.map((s) => { return (
  • {s.name} diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx index 9d42aa389..f3bb9cca7 100644 --- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx +++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx @@ -19,7 +19,6 @@ interface SavePayload { seedRatio?: number; seedTime?: number; packSeedTime?: number; - preferMagnetUrl?: boolean; } interface EditIndexerModalContentProps { @@ -31,25 +30,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) { @@ -66,9 +49,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { const [packSeedTime, setPackSeedTime] = useState( null ); - const [preferMagnetUrl, setPreferMagnetUrl] = useState< - null | string | boolean - >(null); const save = useCallback(() => { let hasChanges = false; @@ -109,11 +89,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { payload.packSeedTime = packSeedTime as number; } - if (preferMagnetUrl !== null) { - hasChanges = true; - payload.preferMagnetUrl = preferMagnetUrl === 'true'; - } - if (hasChanges) { onSavePress(payload); } @@ -127,13 +102,12 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { seedRatio, seedTime, packSeedTime, - preferMagnetUrl, onSavePress, onModalClose, ]); const onInputChange = useCallback( - ({ name, value }: { name: string; value: string }) => { + ({ name, value }) => { switch (name) { case 'enable': setEnable(value); @@ -156,9 +130,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { case 'packSeedTime': setPackSeedTime(value); break; - case 'preferMagnetUrl': - setPreferMagnetUrl(value); - break; default: console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); } @@ -237,7 +208,6 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { name="seedRatio" value={seedRatio} helpText={translate('SeedRatioHelpText')} - isFloat={true} onChange={onInputChange} /> @@ -267,23 +237,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) { onChange={onInputChange} /> - - - {translate('PreferMagnetUrl')} - - -
    - {translate('CountIndexersSelected', { count: selectedCount })} + {translate('CountIndexersSelected', [selectedCount])}
    diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx index d6fc776d6..bd7682018 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx @@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate'; interface IndexerIndexSelectAllButtonProps { label: string; isSelectMode: boolean; - overflowComponent: React.FunctionComponent; + overflowComponent: React.FunctionComponent; } function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) { diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx index 64fe8c1cb..953d0daf9 100644 --- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx +++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx @@ -15,16 +15,6 @@ 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, (indexers) => { @@ -70,7 +60,7 @@ function IndexerIndexSelectFooter() { }, [setIsEditModalOpen]); const onSavePress = useCallback( - (payload: SavePayload) => { + (payload) => { setIsSavingIndexer(true); setIsEditModalOpen(false); @@ -93,7 +83,7 @@ function IndexerIndexSelectFooter() { }, [setIsTagsModalOpen]); const onApplyTagsPress = useCallback( - (tags: number[], applyTags: string) => { + (tags, applyTags) => { setIsSavingTags(true); setIsTagsModalOpen(false); @@ -165,7 +155,7 @@ function IndexerIndexSelectFooter() {
    - {translate('CountIndexersSelected', { count: selectedCount })} + {translate('CountIndexersSelected', [selectedCount])}
    ; + overflowComponent: React.FunctionComponent; onPress: () => void; } diff --git a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx index 1964d271c..964d9ad57 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,7 +12,6 @@ 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'; @@ -28,35 +26,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] diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx index 8e30532cc..5f742d902 100644 --- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx +++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx @@ -1,8 +1,6 @@ -import { uniqBy } from 'lodash'; import React from 'react'; import Label from 'Components/Label'; import { IndexerCapabilities } from 'Indexer/Indexer'; -import translate from 'Utilities/String/translate'; interface CapabilitiesLabelProps { capabilities: IndexerCapabilities; @@ -25,21 +23,17 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) { ); } - const indexerCategories = uniqBy(filteredList, 'id').sort( - (a, b) => a.id - b.id + const nameList = Array.from( + new Set(filteredList.map((item) => item.name).sort()) ); return ( - {indexerCategories.map((category) => { - return ( - - ); + {nameList.map((category) => { + return ; })} - {filteredList.length === 0 ? : null} + {filteredList.length === 0 ? : null} ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css index a20efded3..a0a0daee4 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css @@ -11,12 +11,6 @@ flex: 0 0 60px; } -.id { - composes: cell; - - flex: 0 0 60px; -} - .sortName { composes: cell; @@ -29,8 +23,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { composes: cell; flex: 0 0 90px; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts index 42821bd74..c5d22cf6d 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts @@ -8,10 +8,8 @@ interface CssExports { 'cell': string; 'checkInput': string; 'externalLink': string; - 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx index e4c3cd32e..5325028e9 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSelect } from 'App/SelectContext'; -import CheckInput from 'Components/Form/CheckInput'; +import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import Column from 'Components/Table/Column'; @@ -12,13 +12,11 @@ import { icons } from 'Helpers/Props'; import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector'; -import Indexer from 'Indexer/Indexer'; import IndexerTitleLink from 'Indexer/IndexerTitleLink'; -import { SelectStateInputProps } from 'typings/props'; +import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import translate from 'Utilities/String/translate'; import CapabilitiesLabel from './CapabilitiesLabel'; import IndexerStatusCell from './IndexerStatusCell'; -import PrivacyLabel from './PrivacyLabel'; import ProtocolLabel from './ProtocolLabel'; import styles from './IndexerIndexRow.css'; @@ -27,14 +25,13 @@ interface IndexerIndexRowProps { sortKey: string; columns: Column[]; isSelectMode: boolean; - onCloneIndexerPress(id: number): void; } function IndexerIndexRow(props: IndexerIndexRowProps) { - const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props; + const { indexerId, columns, isSelectMode } = props; const { indexer, appProfile, status, longDateFormat, timeFormat } = - useSelector(createIndexerIndexItemSelector(indexerId)); + useSelector(createIndexerIndexItemSelector(props.indexerId)); const { id, @@ -49,7 +46,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { fields, added, capabilities, - } = indexer as Indexer; + } = indexer; const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? @@ -75,10 +72,6 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime') ?.value ?? undefined; - const preferMagnetUrl = - fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl') - ?.value ?? undefined; - const rssUrl = `${window.location.origin}${ window.Prowlarr.urlBase }/${id}/api?apikey=${encodeURIComponent( @@ -112,7 +105,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { }, []); const onSelectedChange = useCallback( - ({ id, value, shiftKey }: SelectStateInputProps) => { + ({ id, value, shiftKey }) => { selectDispatch({ type: 'toggleSelected', id, @@ -156,25 +149,12 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { ); } - if (name === 'id') { - return ( - - - - ); - } - if (name === 'sortName') { return ( ); @@ -183,7 +163,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { if (name === 'privacy') { return ( - + ); } @@ -222,9 +202,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) { if (name === 'added') { return ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ts(2739) - - {preferMagnetUrl === undefined ? null : ( - - )} - - ); - } - if (name === 'actions') { return ( ; + scrollerRef: React.MutableRefObject; isSelectMode: boolean; isSmallScreen: boolean; - onCloneIndexerPress(id: number): void; } const columnsSelector = createSelector( - (state: AppState) => state.indexerIndex.columns, + (state) => state.indexerIndex.columns, (columns) => columns ); -function Row({ index, style, data }: ListChildComponentProps) { - const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data; +const Row: React.FC> = ({ + index, + style, + data, +}) => { + const { items, sortKey, columns, isSelectMode } = data; if (index >= items.length) { return null; @@ -62,18 +64,16 @@ function Row({ index, style, data }: ListChildComponentProps) { justifyContent: 'space-between', ...style, }} - className={styles.row} >
); -} +}; function getWindowScrollTopPosition() { return document.documentElement.scrollTop || document.body.scrollTop || 0; @@ -88,25 +88,25 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { isSelectMode, isSmallScreen, scrollerRef, - onCloneIndexerPress, } = props; const columns = useSelector(columnsSelector); - const listRef = useRef>(null); + const { showBanners } = useSelector(selectTableOptions); + const listRef = useRef(null); 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({ - width: windowWidth, - height: windowHeight, + width: window.innerWidth, + height: window.innerHeight, }); return; @@ -119,10 +119,10 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { setSize({ width: width - padding * 2, - height: windowHeight, + height: window.innerHeight, }); } - }, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]); + }, [isSmallScreen, scrollerRef, bounds]); useEffect(() => { const currentScrollerRef = scrollerRef.current as HTMLElement; @@ -165,7 +165,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) { } listRef.current?.scrollTo(scrollTop); - scrollerRef?.current?.scrollTo(0, scrollTop); + scrollerRef.current?.scrollTo(0, scrollTop); } } }, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]); @@ -177,6 +177,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..90ad3b0e9 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'; @@ -22,8 +16,7 @@ .minimumSeeders, .seedRatio, .seedTime, -.packSeedTime, -.preferMagnetUrl { +.packSeedTime { 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..94d39a9bb 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.css.d.ts @@ -5,10 +5,8 @@ interface CssExports { 'added': string; 'appProfileId': string; 'capabilities': string; - 'id': string; 'minimumSeeders': string; 'packSeedTime': string; - 'preferMagnetUrl': string; 'priority': string; 'privacy': string; 'protocol': string; diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx index 908be76b5..965451fcf 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableHeader.tsx @@ -14,11 +14,12 @@ import { setIndexerSort, setIndexerTableOption, } from 'Store/Actions/indexerIndexActions'; -import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; import IndexerIndexTableOptions from './IndexerIndexTableOptions'; import styles from './IndexerIndexTableHeader.css'; interface IndexerIndexTableHeaderProps { + showBanners: boolean; columns: Column[]; sortKey?: string; sortDirection?: SortDirection; @@ -31,21 +32,21 @@ 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 }: SelectStateInputProps) => { selectDispatch({ type: value ? 'selectAll' : 'unselectAll', }); @@ -92,18 +93,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..aa4611b97 100644 --- a/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx +++ b/frontend/src/Indexer/Index/Table/IndexerIndexTableOptions.tsx @@ -1,10 +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'; @@ -20,7 +19,7 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { const { showSearchAction } = tableOptions; const onTableOptionChangeWrapper = useCallback( - ({ name, value }: CheckInputChanged) => { + ({ name, value }) => { onTableOptionChange({ tableOptions: { ...tableOptions, @@ -32,17 +31,19 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) { ); return ( - - {translate('ShowSearch')} + + + {translate('ShowSearch')} - - + + + ); } diff --git a/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx b/frontend/src/Indexer/Index/Table/IndexerStatusCell.tsx index 1a2350302..7a5f5cc10 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 + ? translate('EnabledRedirected') + : translate('Enabled'); + return ( - + { + + } {status ? ( - {translate(firstCharToUpper(privacy))} - - ); -} - -export default PrivacyLabel; diff --git a/frontend/src/Indexer/Index/Table/ProtocolLabel.css b/frontend/src/Indexer/Index/Table/ProtocolLabel.css index 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..9056f70f5 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'; @@ -22,23 +21,31 @@ 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 +57,265 @@ 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')} - +
+
+ + + + + + {vipExpiration ? ( + + ) : null} + + {translate('IndexerSite')} + + + + {baseUrl.replace(/(:\/\/)api\./, '$1')} + + + + {protocol === 'usenet' + ? translate('NewznabUrl') + : translate('TorznabUrl')} + + + {`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`} + + {tags.length > 0 ? ( + <> + + {translate('Tags')} + + + + + + ) : null} + +
+
- - {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) => { +
+
+ + + + {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 ( + + ); + }) + } + /> + +
+ + + {capabilities.categories !== null && + capabilities.categories.length > 0 ? ( +
+ + {capabilities.categories + .sort((a, b) => a.id - b.id) + .map((category) => { + return ( + + + {category.id} + {category.name} + + {category.subCategories !== null && + category.subCategories.length > 0 + ? category.subCategories + .sort((a, b) => a.id - b.id) + .map((subCategory) => { + return ( + + {subCategory.id} + + {subCategory.name} + + + ); + }) + : null} + + ); + })} +
+
+ ) : null}
- -
- - -
-
- - -
+ + + +
); 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/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/Indexer/Stats/Stats.css.d.ts similarity index 74% rename from frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts rename to frontend/src/Indexer/Stats/Stats.css.d.ts index fc9081492..ce2364202 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.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 { - 'commandName': string; - 'userAgent': 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..30bff5f84 --- /dev/null +++ b/frontend/src/Indexer/Stats/Stats.js @@ -0,0 +1,261 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +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 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 translate from 'Utilities/String/translate'; +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, + 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..863ccc003 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: translate('BasicSearch') }, + { key: 'tvsearch', value: translate('TvSearch') }, + { key: 'movie', value: translate('MovieSearch') }, + { key: 'music', value: translate( 'AudioSearch') }, + { key: 'book', value: translate('BookSearch') } ]; const seriesTokens = [ 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..404ad7e79 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -24,7 +24,6 @@ class SearchFooter extends Component { super(props, context); const { - defaultSearchQueryParams, defaultIndexerIds, defaultCategories, defaultSearchQuery, @@ -34,16 +33,16 @@ class SearchFooter extends Component { } = 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: defaultSearchLimit, + searchOffset: defaultSearchOffset, + newSearch: true }; } @@ -190,13 +189,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 = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', [searchIndexerIds.length]); if (isPopulated) { - footerLabel = selectedCount === 0 ? translate('FoundCountReleases', { itemCount }) : translate('SelectedCountOfCountReleases', { selectedCount, itemCount }); + footerLabel = selectedCount === 0 ? translate('FoundCountReleases', [itemCount]) : translate('SelectedCountOfCountReleases', [selectedCount, itemCount]); } return ( @@ -212,11 +210,7 @@ class SearchFooter extends Component { name="searchQuery" value={searchQuery} buttons={ - + @@ -265,10 +259,11 @@ class SearchFooter extends Component { />
+ { isPopulated && ); diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js index d12635070..ba25b0912 100644 --- a/frontend/src/Search/SearchIndex.js +++ b/frontend/src/Search/SearchIndex.js @@ -30,7 +30,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 { @@ -72,7 +78,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 +100,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() { @@ -282,10 +281,10 @@ class SearchIndex extends Component { const ViewComponent = getViewComponent(isSmallScreen); const isLoaded = !!(!error && isPopulated && items.length && this.scrollerRef.current); - const hasNoSearchResults = !totalItems; + const hasNoIndexer = !totalItems; return ( - + @@ -314,7 +313,7 @@ class SearchIndex extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} - isDisabled={hasNoSearchResults} + isDisabled={hasNoIndexer} onFilterSelect={onFilterSelect} /> @@ -327,17 +326,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,18 +359,16 @@ 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..5c076c521 --- /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 Tooltip from 'Components/Tooltip/Tooltip'; +import { kinds, tooltipPositions } from 'Helpers/Props'; + +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/Applications/ApplicationSettings.js b/frontend/src/Settings/Applications/ApplicationSettings.js new file mode 100644 index 000000000..d06b7fb65 --- /dev/null +++ b/frontend/src/Settings/Applications/ApplicationSettings.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +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 translate from 'Utilities/String/translate'; +import ApplicationsConnector from './Applications/ApplicationsConnector'; +import ManageApplicationsModal from './Applications/Manage/ManageApplicationsModal'; + +class ApplicationSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isManageApplicationsOpen: false + }; + } + + // + // Listeners + + onManageApplicationsPress = () => { + this.setState({ isManageApplicationsOpen: true }); + }; + + onManageApplicationsModalClose = () => { + this.setState({ isManageApplicationsOpen: false }); + }; + + // + // Render + + render() { + const { + isTestingAll, + isSyncingIndexers, + onTestAllPress, + onAppIndexerSyncPress + } = this.props; + + const { isManageApplicationsOpen } = this.state; + + 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..aebb015a9 100644 --- a/frontend/src/Settings/Applications/Applications/Application.js +++ b/frontend/src/Settings/Applications/Applications/Application.js @@ -2,10 +2,9 @@ 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 +56,19 @@ class Application extends Component { const { id, name, - enable, syncLevel, - fields, tags, tagList } = this.props; - const applicationUrl = fields.find((field) => field.name === 'baseUrl')?.value; - return ( -
-
- {name} -
- - { - enable && applicationUrl ? - : null - } +
+ {name}
{ @@ -128,7 +111,7 @@ class Application extends Component { isOpen={this.state.isDeleteApplicationModalOpen} kind={kinds.DANGER} title={translate('DeleteApplication')} - message={translate('DeleteApplicationMessageText', { name })} + message={translate('DeleteApplicationMessageText', [name])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteApplication} onCancel={this.onDeleteApplicationModalClose} @@ -141,9 +124,7 @@ class Application extends Component { Application.propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, - enable: PropTypes.bool.isRequired, syncLevel: PropTypes.string.isRequired, - fields: PropTypes.arrayOf(PropTypes.object).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteApplication: PropTypes.func diff --git a/frontend/src/Settings/Applications/Applications/Applications.js b/frontend/src/Settings/Applications/Applications/Applications.js index 66d02088e..c6421c9ec 100644 --- a/frontend/src/Settings/Applications/Applications/Applications.js +++ b/frontend/src/Settings/Applications/Applications/Applications.js @@ -62,7 +62,7 @@ class Applications extends Component { return (
diff --git a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js index 9f5e570c5..bbf8722c5 100644 --- a/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js +++ b/frontend/src/Settings/Applications/Applications/ApplicationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Applications from './Applications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.applications', sortByProp('name')), + createSortedSectionSelector('settings.applications', sortByName), createTagsSelector(), (applications, tagList) => { return { diff --git a/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js b/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js index 00e30cdb7..a8b0636de 100644 --- a/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js +++ b/frontend/src/Settings/Applications/Applications/EditApplicationModalContent.js @@ -19,24 +19,9 @@ 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) { @@ -60,7 +45,6 @@ function EditApplicationModalContent(props) { const { id, - implementationName, name, syncLevel, tags, @@ -71,7 +55,7 @@ function EditApplicationModalContent(props) { return ( - {id ? translate('EditApplicationImplementation', { implementationName }) : translate('AddApplicationImplementation', { implementationName })} + {`${id ? translate('Edit') : translate('Add')} ${translate('Application')}`} @@ -118,10 +102,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 +114,7 @@ function EditApplicationModalContent(props) { diff --git a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx index 57e88a4fe..dfdd1639b 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/Edit/ManageApplicationsEditModalContent.tsx @@ -25,31 +25,10 @@ interface ManageApplicationsEditModalContentProps { 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'); - }, - }, + { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, + { key: ApplicationSyncLevel.Disabled, value: translate('Disabled') }, + { key: ApplicationSyncLevel.AddOnly, value: translate('AddOnly') }, + { key: ApplicationSyncLevel.FullSync, value: translate('FullSync') }, ]; function ManageApplicationsEditModalContent( @@ -103,10 +82,9 @@ function ManageApplicationsEditModalContent( name="syncLevel" value={syncLevel} values={syncLevelOptions} - helpTexts={[ - translate('SyncLevelAddRemove'), - translate('SyncLevelFull'), - ]} + helpText={`${translate('SyncLevelAddRemove')}
${translate( + 'SyncLevelFull' + )}`} onChange={onInputChange} /> @@ -114,7 +92,7 @@ function ManageApplicationsEditModalContent(
- {translate('CountApplicationsSelected', { count: selectedCount })} + {translate('CountApplicationsSelected', [selectedCount])}
diff --git a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx index bb81729f3..b6c636fbe 100644 --- a/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx +++ b/frontend/src/Settings/Applications/Applications/Manage/ManageApplicationsModalContent.tsx @@ -14,11 +14,9 @@ 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'; @@ -38,25 +36,25 @@ type OnSelectedChangeCallback = React.ComponentProps< const COLUMNS = [ { name: 'name', - label: () => translate('Name'), + label: translate('Name'), isSortable: true, isVisible: true, }, { name: 'implementation', - label: () => translate('Implementation'), + label: translate('Implementation'), isSortable: true, isVisible: true, }, { name: 'syncLevel', - label: () => translate('SyncLevel'), + label: translate('SyncLevel'), isSortable: true, isVisible: true, }, { name: 'tags', - label: () => translate('Tags'), + label: translate('Tags'), isSortable: true, isVisible: true, }, @@ -64,8 +62,6 @@ const COLUMNS = [ interface ManageApplicationsModalContentProps { onModalClose(): void; - sortKey?: string; - sortDirection?: SortDirection; } function ManageApplicationsModalContent( @@ -80,8 +76,6 @@ function ManageApplicationsModalContent( isSaving, error, items, - sortKey, - sortDirection, }: ApplicationAppState = useSelector( createClientSideCollectionSelector('settings.applications') ); @@ -102,13 +96,6 @@ function ManageApplicationsModalContent( const selectedCount = selectedIds.length; - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageApplicationsSort({ sortKey: value })); - }, - [dispatch] - ); - const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -213,10 +200,7 @@ function ManageApplicationsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} - sortKey={sortKey} - sortDirection={sortDirection} onSelectAllChange={onSelectAllChange} - onSortPress={onSortPress} > {items.map((item) => { @@ -268,9 +252,9 @@ function ManageApplicationsModalContent( 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..7136f531e 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -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..b9cb2cc9c 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}`} @@ -221,12 +219,6 @@ 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/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index d18e694c9..c9279de3b 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -25,25 +25,9 @@ interface ManageDownloadClientsEditModalContentProps { 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'); - }, - }, + { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, + { key: 'enabled', value: translate('Enabled') }, + { key: 'disabled', value: translate('Disabled') }, ]; function ManageDownloadClientsEditModalContent( @@ -129,7 +113,7 @@ function ManageDownloadClientsEditModalContent(
- {translate('CountDownloadClientsSelected', { count: selectedCount })} + {translate('CountDownloadClientsSelected', [selectedCount])}
diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index fa82d61b9..c7291b012 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -14,11 +14,9 @@ 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'; @@ -37,25 +35,25 @@ type OnSelectedChangeCallback = React.ComponentProps< const COLUMNS = [ { name: 'name', - label: () => translate('Name'), + label: translate('Name'), isSortable: true, isVisible: true, }, { name: 'implementation', - label: () => translate('Implementation'), + label: translate('Implementation'), isSortable: true, isVisible: true, }, { name: 'enable', - label: () => translate('Enabled'), + label: translate('Enabled'), isSortable: true, isVisible: true, }, { name: 'priority', - label: () => translate('ClientPriority'), + label: translate('ClientPriority'), isSortable: true, isVisible: true, }, @@ -63,8 +61,6 @@ const COLUMNS = [ interface ManageDownloadClientsModalContentProps { onModalClose(): void; - sortKey?: string; - sortDirection?: SortDirection; } function ManageDownloadClientsModalContent( @@ -79,8 +75,6 @@ function ManageDownloadClientsModalContent( isSaving, error, items, - sortKey, - sortDirection, }: DownloadClientAppState = useSelector( createClientSideCollectionSelector('settings.downloadClients') ); @@ -99,13 +93,6 @@ function ManageDownloadClientsModalContent( const selectedCount = selectedIds.length; - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageDownloadClientsSort({ sortKey: value })); - }, - [dispatch] - ); - const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -186,10 +173,7 @@ function ManageDownloadClientsModalContent( selectAll={true} allSelected={allSelected} allUnselected={allUnselected} - sortKey={sortKey} - sortDirection={sortDirection} onSelectAllChange={onSelectAllChange} - onSortPress={onSortPress} > {items.map((item) => { @@ -233,18 +217,18 @@ function ManageDownloadClientsModalContent( diff --git a/frontend/src/Settings/General/LoggingSettings.js b/frontend/src/Settings/General/LoggingSettings.js index 61a259258..540e29b01 100644 --- a/frontend/src/Settings/General/LoggingSettings.js +++ b/frontend/src/Settings/General/LoggingSettings.js @@ -15,14 +15,12 @@ const logLevelOptions = [ function LoggingSettings(props) { const { - advancedSettings, settings, onInputChange } = props; const { - logLevel, - logSizeLimit + logLevel } = settings; return ( @@ -39,30 +37,11 @@ function LoggingSettings(props) { {...logLevel} /> - - - {translate('LogSizeLimit')} - - - ); } LoggingSettings.propTypes = { - advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/General/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..0ffd49b40 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -137,7 +137,7 @@ class Notification extends Component { isOpen={this.state.isDeleteNotificationModalOpen} kind={kinds.DANGER} title={translate('DeleteNotification')} - message={translate('DeleteNotificationMessageText', { name })} + message={translate('DeleteNotificationMessageText', [name])} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteNotification} onCancel={this.onDeleteNotificationModalClose} diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index 6351c6f8a..b306f742a 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; +import sortByName from 'Utilities/Array/sortByName'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByProp('name')), + createSortedSectionSelector('settings.notifications', sortByName), createTagsSelector(), (notifications, tagList) => { return { 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..614fd2fe0 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [ ]; export const weekColumnOptions = [ - { key: 'ddd M/D', value: 'Tue 3/25', 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 = [ @@ -147,7 +147,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/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/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..357588b75 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 @@ -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..9f7111960 100644 --- a/frontend/src/Store/Actions/Settings/applications.js +++ b/frontend/src/Store/Actions/Settings/applications.js @@ -1,5 +1,4 @@ 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'; @@ -8,7 +7,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -32,10 +30,9 @@ 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 TEST_ALL_APPLICATIONS = 'indexers/testAllApplications'; export const BULK_EDIT_APPLICATIONS = 'settings/applications/bulkEditApplications'; export const BULK_DELETE_APPLICATIONS = 'settings/applications/bulkDeleteApplications'; -export const SET_MANAGE_APPLICATIONS_SORT = 'settings/applications/setManageApplicationsSort'; // // Action Creators @@ -52,7 +49,6 @@ 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 { @@ -92,14 +88,7 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); - } - } + pendingChanges: {} }, // @@ -128,14 +117,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/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js index 56784d5d0..b4513e7c1 100644 --- a/frontend/src/Store/Actions/Settings/downloadClients.js +++ b/frontend/src/Store/Actions/Settings/downloadClients.js @@ -1,5 +1,4 @@ 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'; @@ -8,7 +7,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import { createThunk } from 'Store/thunks'; @@ -36,7 +34,6 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients'; export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients'; -export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort'; // // Action Creators @@ -53,7 +50,6 @@ 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 { @@ -93,14 +89,7 @@ export default { isTesting: false, isTestingAll: false, items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: function(item) { - return item.name.toLowerCase(); - } - } + pendingChanges: {} }, // @@ -153,15 +142,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..20edb2099 100644 --- a/frontend/src/Store/Actions/Settings/indexerProxies.js +++ b/frontend/src/Store/Actions/Settings/indexerProxies.js @@ -104,8 +104,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..de927ba4f 100644 --- a/frontend/src/Store/Actions/Settings/notifications.js +++ b/frontend/src/Store/Actions/Settings/notifications.js @@ -104,7 +104,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..c8019e6d4 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,67 @@ 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'), + label: translate('GrabTitle'), isSortable: false, isVisible: false }, { name: 'queryType', - label: () => translate('QueryType'), + label: translate('QueryType'), 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 +101,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 +117,7 @@ export const defaultState = { }, { key: 'indexerRss', - label: () => translate('IndexerRss'), + label: translate('IndexerRss'), filters: [ { key: 'eventType', @@ -134,7 +128,7 @@ export const defaultState = { }, { key: 'indexerQuery', - label: () => translate('IndexerQuery'), + label: translate('IndexerQuery'), filters: [ { key: 'eventType', @@ -145,7 +139,7 @@ export const defaultState = { }, { key: 'indexerAuth', - label: () => translate('IndexerAuth'), + label: translate('IndexerAuth'), filters: [ { key: 'eventType', @@ -156,7 +150,7 @@ export const defaultState = { }, { key: 'failed', - label: () => translate('Failed'), + label: translate('Failed'), filters: [ { key: 'successful', @@ -165,27 +159,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..3a2e7d5ce 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'; @@ -20,7 +16,6 @@ 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'; // @@ -59,7 +54,7 @@ export const defaultState = { export const filters = [ { key: 'all', - label: () => translate('All'), + label: translate('All'), filters: [] } ]; @@ -74,68 +69,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 +88,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'; @@ -166,7 +107,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); @@ -244,16 +184,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 +201,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..a42897996 100644 --- a/frontend/src/Store/Actions/indexerIndexActions.js +++ b/frontend/src/Store/Actions/indexerIndexActions.js @@ -32,105 +32,93 @@ export const defaultState = { columns: [ { name: 'status', - columnLabel: () => translate('IndexerStatus'), + columnLabel: translate('IndexerStatus'), 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'), + label: translate('MinimumSeeders'), isSortable: true, isVisible: false }, { name: 'seedRatio', - label: () => translate('SeedRatio'), + label: translate('SeedRatio'), isSortable: true, isVisible: false }, { name: 'seedTime', - label: () => translate('SeedTime'), + label: translate('SeedTime'), isSortable: true, isVisible: false }, { name: 'packSeedTime', - label: () => translate('PackSeedTime'), - isSortable: true, - isVisible: false - }, - { - name: 'preferMagnetUrl', - label: () => translate('PreferMagnetUrl'), + label: translate('PackSeedTime'), 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 +136,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 } 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..298357a82 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'; @@ -58,71 +56,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 +158,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 +350,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 +381,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/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/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/Themes/dark.js b/frontend/src/Styles/Themes/dark.js index a7cbb6de0..657568982 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', @@ -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..4f9cd0483 100644 --- a/frontend/src/System/Backup/Backups.js +++ b/frontend/src/System/Backup/Backups.js @@ -21,17 +21,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 }, { @@ -109,7 +109,7 @@ class Backups extends Component { { !isFetching && !!error && - {translate('BackupsLoadError')} + {translate('UnableToLoadBackups')} } 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.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..6a933cf2f --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,252 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +class Updates extends Component { + + // + // Render + + render() { + const { + currentVersion, + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + isInstallingUpdate, + updateMechanism, + isDocker, + updateMechanismMessage, + shortDateFormat, + longDateFormat, + timeFormat, + onInstallLatestPress + } = this.props; + + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const externalUpdaterPrefix = 'Unable to update Prowlarr directly,'; + const externalUpdaterMessages = { + external: 'Prowlarr is configured to use an external update mechanism', + apt: 'use apt to install the update', + docker: 'update the docker container to receive the update' + }; + + return ( + + + { + !isPopulated && !hasError && + + } + + { + noUpdates && + + {translate('NoUpdatesAreAvailable')} + + } + + { + hasUpdateToInstall && +
+ { + (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? + + Install Latest + : + + + + +
+ {externalUpdaterPrefix} +
+
+ } + + { + isFetching && + + } +
+ } + + { + noUpdateToInstall && +
+ + +
+ {translate('TheLatestVersionIsAlreadyInstalled', ['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/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..7483b27aa --- /dev/null +++ b/frontend/src/Utilities/String/translate.js @@ -0,0 +1,31 @@ +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 = []) { + const translation = translations[key] || key; + + if (args) { + return translation.replace(/\{(\d+)\}/g, (match, index) => { + return args[index]; + }); + } + + return translation; +} 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/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/Indexer.ts b/frontend/src/typings/Indexer.ts new file mode 100644 index 000000000..6e31aa0c6 --- /dev/null +++ b/frontend/src/typings/Indexer.ts @@ -0,0 +1,27 @@ +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 Indexer extends ModelBase { + enable: boolean; + appProfileId: number; + protocol: string; + priority: number; + name: string; + fields: Field[]; + implementationName: string; + implementation: string; + configContract: string; + infoLink: string; + tags: number[]; +} + +export default Indexer; 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 index 63ea906c4..e2b5ad7eb 100644 --- a/frontend/src/typings/Notification.ts +++ b/frontend/src/typings/Notification.ts @@ -13,15 +13,6 @@ export interface Field { 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; 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/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/Settings/UiSettings.ts b/frontend/src/typings/UiSettings.ts similarity index 59% rename from frontend/src/typings/Settings/UiSettings.ts rename to frontend/src/typings/UiSettings.ts index 656c4518b..79cb0f333 100644 --- a/frontend/src/typings/Settings/UiSettings.ts +++ b/frontend/src/typings/UiSettings.ts @@ -1,5 +1,4 @@ -export default interface UiSettings { - theme: 'auto' | 'dark' | 'light'; +export interface UiSettings { showRelativeDates: boolean; shortDateFormat: string; longDateFormat: string; 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/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..74a1e4694 100644 --- a/package.json +++ b/package.json @@ -11,46 +11,47 @@ "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" ], "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.51.2", + "@sentry/integrations": "7.51.2", + "@types/node": "18.15.11", + "@types/react": "18.2.6", + "@types/react-dom": "18.2.4", + "chart.js": "4.3.0", + "classnames": "2.3.2", + "clipboard": "2.0.11", "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.7.0", "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,81 +65,83 @@ "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-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.4" }, "devDependencies": { - "@babel/core": "7.26.0", - "@babel/eslint-parser": "7.25.9", - "@babel/plugin-proposal-export-default-from": "7.25.9", + "@babel/core": "7.22.9", + "@babel/eslint-parser": "7.22.9", + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-export-default-from": "7.22.5", + "@babel/plugin-proposal-export-namespace-from": "7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-proposal-optional-chaining": "7.21.0", "@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.22.9", + "@babel/preset-react": "7.22.5", + "@babel/preset-typescript": "7.22.5", + "@types/react-window": "1.8.5", + "@types/webpack-livereload-plugin": "2.3.3", + "@typescript-eslint/eslint-plugin": "5.59.5", + "@typescript-eslint/parser": "5.59.5", "are-you-es5": "2.1.2", - "autoprefixer": "10.4.20", - "babel-loader": "9.2.1", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.3", "babel-plugin-inline-classnames": "2.0.1", "babel-plugin-transform-react-remove-prop-types": "0.4.24", - "core-js": "3.39.0", + "core-js": "3.31.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.45.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.1", "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.23", "postcss-color-function": "4.1.0", "postcss-loader": "7.3.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", "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", + "stylelint-order": "6.0.3", + "terser-webpack-plugin": "5.3.9", + "ts-loader": "9.4.2", "typescript-plugin-css-modules": "5.0.1", "url-loader": "4.1.1", - "webpack": "5.95.0", + "webpack": "5.88.1", "webpack-cli": "5.1.4", "webpack-livereload-plugin": "3.0.2" } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ce3672c38..a926a45d4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -99,52 +99,20 @@ $(MSBuildProjectName.Replace('Prowlarr','NzbDrone')) - - - - - - - - - - - - - - - - - - true - - - - true - - true - - - - - - - - - - - - - - false + + + + + + + + + @@ -176,52 +144,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/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs index 02078c47e..a32084602 100644 --- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs +++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs @@ -39,10 +39,6 @@ namespace NzbDrone.Automation.Test.PageModel var 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/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index e9d4aa3b0..dd32e187d 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -1,12 +1,10 @@ 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] diff --git a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs index dd27f6f1b..1dc4256ee 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Common.Test.DiskTests .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] @@ -62,7 +62,7 @@ namespace NzbDrone.Common.Test.DiskTests .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] diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index fac3e20e7..4fca6ca40 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -564,7 +564,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 +584,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 +601,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 +618,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 +638,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 +655,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 +837,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 +875,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/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/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 43620edf4..4181057d6 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() { diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 9e2b31d87..d7c3c9106 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -29,8 +29,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 +43,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")] @@ -91,16 +87,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests // 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 +102,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..a33a53c01 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,7 +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(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")] [TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")] public void Clean_Path_Windows(string dirty, string clean) { @@ -133,16 +132,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 +144,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 +165,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 +315,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..4cd6f6835 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) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 621d4b258..521cb61df 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()) { @@ -149,34 +149,25 @@ namespace NzbDrone.Common.Disk return Directory.EnumerateFileSystemEntries(path).Empty(); } - public IEnumerable GetDirectories(string path) + public string[] GetDirectories(string path) { Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); - 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); - 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); - 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) @@ -189,25 +180,6 @@ 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; } @@ -316,9 +288,8 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); - 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); } @@ -443,7 +414,7 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); - foreach (var file in GetFiles(path, false)) + foreach (var file in GetFiles(path, SearchOption.TopDirectoryOnly)) { DeleteFile(file); } @@ -544,18 +515,13 @@ namespace NzbDrone.Common.Disk 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); 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/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 46589411a..6c33692c3 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -22,8 +22,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 +52,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..f6f01fccf 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -10,8 +9,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 +96,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 +130,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 +139,6 @@ namespace NzbDrone.Common.Disk public string FullPath => _path; - public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind); - public string FileName { get @@ -201,29 +161,6 @@ namespace NzbDrone.Common.Disk } } - public string Name - { - // Meant to behave similar to DirectoryInfo.Name - get - { - var index = GetFileNameIndex(); - - if (index == -1) - { - return PathWithoutTrailingSlash; - } - - var rootLength = GetRootLength(); - - if (rootLength > index + 1) - { - return _path.Substring(0, rootLength); - } - - return TrimTrailingSlash(_path.Substring(index).TrimStart('/', '\\'), _kind); - } - } - public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs); private int GetFileNameIndex() @@ -253,50 +190,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; @@ -369,11 +267,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 +278,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) { diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs index 178ce7a0f..78caf0b12 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs @@ -159,7 +159,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/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/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/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 30a467f21..f2d82f10e 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,14 +24,10 @@ 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); @@ -41,10 +36,10 @@ namespace NzbDrone.Common.Extensions // UNC if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\")) { - return info.FullName.TrimEnd('/', '\\'); + return info.FullName.TrimEnd('/', '\\', ' '); } - return info.FullName.TrimEnd('/').Trim('\\'); + return info.FullName.TrimEnd('/').Trim('\\', ' '); } public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null) @@ -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; @@ -138,32 +138,11 @@ namespace NzbDrone.Common.Extensions public static bool IsPathValid(this string path, PathValidationType validationType) { - 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); @@ -179,11 +158,6 @@ namespace NzbDrone.Common.Extensions 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; 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/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 29bb49fa5..2401efaef 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,16 +1,13 @@ using System; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; -using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -32,14 +29,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,8 +42,6 @@ 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) @@ -115,59 +107,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()); + + 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); } } @@ -283,27 +268,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 +291,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..a1e6f0243 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -109,7 +109,7 @@ namespace NzbDrone.Common.Http if (response.HasHttpRedirect && !RuntimeInfo.IsProduction) { - _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.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 +221,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 +260,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); } } diff --git a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs index c80044d29..022d8adee 100644 --- a/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs +++ b/src/NzbDrone.Common/Http/Proxy/HttpProxySettings.cs @@ -30,8 +30,7 @@ namespace NzbDrone.Common.Http.Proxy { if (!string.IsNullOrWhiteSpace(BypassFilter)) { - var hostlist = BypassFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - + var hostlist = BypassFilter.Split(','); for (var i = 0; i < hostlist.Length; i++) { if (hostlist[i].StartsWith("*")) diff --git a/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs index 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..13d7e935d 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Common.Instrumentation private static readonly Regex[] CleansingRules = { // Url - new (@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|rsskey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"(?<=[?&: ;])(apikey|api_key|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pid|pwd)=(?[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=[?& ;])[^=]*?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"rss(24h)?\.torrentleech\.org/(?!rss)(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"torrentleech\.org/rss/download/[0-9]+/(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), @@ -21,7 +21,6 @@ namespace NzbDrone.Common.Instrumentation new (@"(?<=authkey = "")(?[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"(?<=beyond-hd\.[a-z]+/torrent/download/[\w\d-]+[.]\d+[.])(?[a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new (@"(?:sharewood)\.[a-z]{2,3}/api/(?[a-z0-9]{16,})/", RegexOptions.Compiled | RegexOptions.IgnoreCase), // UNIT3D new (@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), @@ -31,7 +30,7 @@ namespace NzbDrone.Common.Instrumentation new (@"""/(home|Users)/(?[^/""]+?)(/|$)", 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), + new (@"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), @@ -58,13 +57,10 @@ namespace NzbDrone.Common.Instrumentation // 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) + new (@",""info_hash"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"""token"":""(?[^&=]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@",""pass[- _]?key"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@",""rss[- _]?key"":""(?[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase), }; private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(? + { + c.ForLogger("System.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); + c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); + c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); + }); + } + private static void RegisterConsole() { var level = LogLevel.Trace; @@ -115,12 +122,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 +139,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 +148,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 +171,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 +196,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 +205,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/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 3a4737f74..164c14a7d 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -50,13 +50,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,7 +100,7 @@ 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 => { @@ -114,33 +108,9 @@ namespace NzbDrone.Common.Instrumentation.Sentry 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.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 +118,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 +128,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry { SentrySdk.ConfigureScope(scope => { - scope.User = new SentryUser + scope.User = new User { Id = HashUtil.AnonymousToken() }; @@ -207,7 +177,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 +311,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/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/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index c68207a09..5db0565e0 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) @@ -316,7 +313,7 @@ namespace NzbDrone.Common.Processes processInfo = new ProcessInfo(); processInfo.Id = process.Id; processInfo.Name = process.ProcessName; - processInfo.StartPath = process.MainModule?.FileName; + processInfo.StartPath = process.MainModule.FileName; if (process.Id != GetCurrentProcessId() && process.HasExited) { diff --git a/src/NzbDrone.Common/Prowlarr.Common.csproj b/src/NzbDrone.Common/Prowlarr.Common.csproj index 106890399..4cbb90d63 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/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/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/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.Core.Test/Datastore/BasicRepositoryFixture.cs b/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs index fac7e7f00..8070f9fda 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; }); 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/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/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index 3e6a8f66e..6dbd46c31 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework Mocker.SetConstant(new HttpProxySettingsProvider(Mocker.Resolve())); Mocker.SetConstant(new ManagedWebProxyFactory(Mocker.Resolve())); Mocker.SetConstant(new X509CertificateValidationService(Mocker.Resolve(), TestLogger)); - Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), 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/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 index 39d5a6f61..10a4c8145 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetRequestGeneratorFixture.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests query.Tvrage.Should().BeNull(); query.Search.Should().BeNull(); query.Category.Should().Be("Episode"); - query.Name.Should().Be("S01E03%"); + query.Name.Should().Be("S01E03"); } [Test] @@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests query.Tvrage.Should().BeNull(); query.Search.Should().Be("Malcolm%in%the%Middle"); query.Category.Should().Be("Episode"); - query.Name.Should().Be("S02E03%"); + query.Name.Should().Be("S02E03"); } [Test] diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs index 4bc704ccd..5ff5b5ee9 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; @@ -22,10 +21,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 +37,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 +74,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/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/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..307585d29 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" }; + + var 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/RedactedTests/RedactedFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/RedactedTests/RedactedFixture.cs index d7eb35cd1..a23162b5d 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"); + 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..e3aa89400 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 @@ -41,9 +40,9 @@ 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); 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/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/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/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/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..d35409c0b 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -41,23 +41,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 +82,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..f516fddf1 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,6 +46,7 @@ namespace NzbDrone.Core.Applications yield return new ApplicationDefinition { + Name = GetType().Name, SyncLevel = ApplicationSyncLevel.FullSync, 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/ApplicationFactory.cs b/src/NzbDrone.Core/Applications/ApplicationFactory.cs index 2ecd2e78b..31386ad50 100644 --- a/src/NzbDrone.Core/Applications/ApplicationFactory.cs +++ b/src/NzbDrone.Core/Applications/ApplicationFactory.cs @@ -42,18 +42,13 @@ 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) + if (blockedApplications.TryGetValue(application.Definition.Id, out var blockedApplicationStatus)) { _logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime()); continue; @@ -67,19 +62,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..3434c8aa4 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; } @@ -223,12 +211,10 @@ 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); - 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); - 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..7ed59061c 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.LazyLibrarian } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to LazyLibrarian. {ex.Message}")); } @@ -65,9 +65,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,7 +74,7 @@ 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); @@ -105,28 +103,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 +131,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 +144,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 +154,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/LazyLibrarianV1Proxy.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs index f4c6da138..563f9ed3b 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarianV1Proxy.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net.Http; using FluentValidation.Results; @@ -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; diff --git a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs index 19c842f5c..68137dc4d 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Lidarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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,7 +49,7 @@ 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) { @@ -64,12 +64,11 @@ namespace NzbDrone.Core.Applications.Lidarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Lidarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Lidarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Lidarr URL is invalid, Prowlarr cannot connect to Lidarr - are you missing a URL base?")); break; default: - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Lidarr. {ex.Message}")); break; } @@ -81,7 +80,7 @@ namespace NzbDrone.Core.Applications.Lidarr } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Lidarr. {ex.Message}")); } @@ -97,20 +96,15 @@ namespace NzbDrone.Core.Applications.Lidarr 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 +113,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,7 +122,7 @@ 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); @@ -158,27 +150,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 +193,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,11 +207,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" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" }; if (id == 0) { @@ -254,7 +243,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 +251,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..f53f3d90d 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrSettings.cs @@ -36,12 +36,9 @@ namespace NzbDrone.Core.Applications.Lidarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), 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..40aeaf3c0 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/LidarrV1Proxy.cs @@ -84,7 +84,6 @@ namespace NzbDrone.Core.Applications.Lidarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); try { @@ -104,21 +103,10 @@ namespace NzbDrone.Core.Applications.Lidarr { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); + request.Url = request.Url.AddQueryParam("forceSave", "true"); 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,7 +114,6 @@ 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"); @@ -166,7 +153,6 @@ namespace NzbDrone.Core.Applications.Lidarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs index e9fd9ffe7..7d84feae1 100644 --- a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs +++ b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs @@ -16,8 +16,8 @@ namespace NzbDrone.Core.Applications.Mylar private readonly IMylarV3Proxy _mylarV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Mylar } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Mylar. {ex.Message}")); } @@ -65,9 +65,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,7 +74,7 @@ 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); @@ -105,28 +103,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 +131,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 +144,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 +154,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/Radarr/Radarr.cs b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs index 85b9c4a3b..a3d63317f 100644 --- a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs +++ b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Radarr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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,7 +49,7 @@ 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) { @@ -64,12 +64,11 @@ namespace NzbDrone.Core.Applications.Radarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Radarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Radarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Radarr URL is invalid, Prowlarr cannot connect to Radarr - are you missing a URL base?")); break; default: - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Radarr. {ex.Message}")); break; } @@ -81,7 +80,7 @@ namespace NzbDrone.Core.Applications.Radarr } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Radarr. {ex.Message}")); } @@ -97,20 +96,15 @@ namespace NzbDrone.Core.Applications.Radarr 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 +113,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,7 +122,7 @@ 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); @@ -158,25 +150,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 +193,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,11 +207,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" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; if (id == 0) { @@ -252,18 +243,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..3736b38f2 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs @@ -37,12 +37,9 @@ namespace NzbDrone.Core.Applications.Radarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), 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..6e23077ae 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs @@ -85,7 +85,6 @@ namespace NzbDrone.Core.Applications.Radarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); try { @@ -105,21 +104,10 @@ namespace NzbDrone.Core.Applications.Radarr { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); + request.Url = request.Url.AddQueryParam("forceSave", "true"); 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,7 +115,6 @@ 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"); @@ -179,7 +166,6 @@ namespace NzbDrone.Core.Applications.Radarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs index 1fc6742ae..0860c4c6f 100644 --- a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs +++ b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Readarr private readonly IReadarrV1Proxy _readarrV1Proxy; private readonly IConfigFileProvider _configFileProvider; - public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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,7 +49,7 @@ 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) { @@ -64,12 +64,11 @@ namespace NzbDrone.Core.Applications.Readarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Readarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Readarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Readarr URL is invalid, Prowlarr cannot connect to Readarr - are you missing a URL base?")); break; default: - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Readarr. {ex.Message}")); break; } @@ -81,7 +80,7 @@ namespace NzbDrone.Core.Applications.Readarr } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Readarr. {ex.Message}")); } @@ -91,26 +90,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 +113,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,7 +122,7 @@ 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); @@ -158,27 +150,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 +175,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 +190,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 +204,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 +234,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 +242,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..55a7014bd 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrSettings.cs @@ -37,12 +37,9 @@ namespace NzbDrone.Core.Applications.Readarr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), 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..01302eca7 100644 --- a/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Readarr/ReadarrV1Proxy.cs @@ -81,7 +81,6 @@ namespace NzbDrone.Core.Applications.Readarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); try { @@ -101,21 +100,10 @@ namespace NzbDrone.Core.Applications.Readarr { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); + request.Url = request.Url.AddQueryParam("forceSave", "true"); 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,7 +111,6 @@ 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); @@ -153,7 +140,6 @@ namespace NzbDrone.Core.Applications.Readarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs index 6e5284fc7..a894b9d1c 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Sonarr private readonly ISonarrV3Proxy _sonarrV3Proxy; private readonly IConfigFileProvider _configFileProvider; - public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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,7 +49,7 @@ 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) { @@ -64,16 +64,15 @@ namespace NzbDrone.Core.Applications.Sonarr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Sonarr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Sonarr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Sonarr URL is invalid, Prowlarr cannot connect to Sonarr - are you missing a URL base?")); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Sonarr not found"); + _logger.Error(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"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Sonarr. {ex.Message}")); break; } @@ -85,7 +84,7 @@ namespace NzbDrone.Core.Applications.Sonarr } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Sonarr. {ex.Message}")); } @@ -101,20 +100,15 @@ namespace NzbDrone.Core.Applications.Sonarr 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 +117,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,7 +127,7 @@ 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); @@ -163,27 +155,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 +199,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,11 +213,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" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "animeStandardFormatSearch", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" }; if (id == 0) { @@ -260,8 +249,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())); + 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 (sonarrIndexer.Fields.Any(x => x.Name == "animeStandardFormatSearch")) { @@ -274,11 +263,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..03c4376c4 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs @@ -30,13 +30,10 @@ 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); @@ -61,10 +58,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 +65,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 && animeStandardFormatSearchCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare; } } } diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs index 95b52bab0..9c31df18c 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs @@ -43,11 +43,8 @@ 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; } + [FieldDefinition(5, Label = "Sync Anime Standard Format Search", Type = FieldType.Checkbox, Advanced = true, HelpText = "Sync also searching for anime using the standard numbering")] + public bool SyncAnimeStandardFormatSearch { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs index f92043c99..19eed7fae 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs @@ -84,7 +84,6 @@ namespace NzbDrone.Core.Applications.Sonarr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); try { @@ -104,21 +103,10 @@ namespace NzbDrone.Core.Applications.Sonarr { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); + request.Url = request.Url.AddQueryParam("forceSave", "true"); 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,7 +114,6 @@ 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"); @@ -166,7 +153,6 @@ namespace NzbDrone.Core.Applications.Sonarr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs index 0c149fc7c..96076e61f 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs @@ -22,8 +22,8 @@ namespace NzbDrone.Core.Applications.Whisparr private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; - public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, 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,7 +49,7 @@ 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) { @@ -64,12 +64,11 @@ namespace NzbDrone.Core.Applications.Whisparr failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Whisparr cannot connect to Prowlarr")); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "Whisparr returned redirect and is invalid"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Whisparr URL is invalid, Prowlarr cannot connect to Whisparr - are you missing a URL base?")); break; default: - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Whisparr. {ex.Message}")); break; } @@ -81,7 +80,7 @@ namespace NzbDrone.Core.Applications.Whisparr } catch (Exception ex) { - _logger.Warn(ex, "Unable to complete application test"); + _logger.Error(ex, "Unable to complete application test"); failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Whisparr. {ex.Message}")); } @@ -91,26 +90,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 +113,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,7 +122,7 @@ 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); @@ -158,27 +150,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 +175,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 +190,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 +204,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 +234,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..5fe636747 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrSettings.cs @@ -37,12 +37,9 @@ namespace NzbDrone.Core.Applications.Whisparr [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), 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..c81a6e149 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/WhisparrV3Proxy.cs @@ -81,7 +81,6 @@ namespace NzbDrone.Core.Applications.Whisparr var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); - request.ContentSummary = indexer.ToJson(Formatting.None); try { @@ -99,21 +98,10 @@ namespace NzbDrone.Core.Applications.Whisparr { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put); + request.Url = request.Url.AddQueryParam("forceSave", "true"); 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,7 +109,6 @@ 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); @@ -151,7 +138,6 @@ namespace NzbDrone.Core.Applications.Whisparr _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 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..c2731e084 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() @@ -165,7 +144,7 @@ namespace NzbDrone.Core.Configuration { const string defaultValue = "*"; - var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue); + var bindAddress = GetValue("BindAddress", defaultValue); if (string.IsNullOrWhiteSpace(bindAddress)) { return defaultValue; @@ -175,19 +154,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 +182,7 @@ namespace NzbDrone.Core.Configuration { get { - var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false); + var enabled = GetValueBoolean("AuthenticationEnabled", false, false); if (enabled) { @@ -211,92 +190,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 +277,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 +325,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 +366,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 +396,6 @@ namespace NzbDrone.Core.Configuration public void HandleAsync(ApplicationStartedEvent message) { - MigrateConfigFile(); EnsureDefaultConfigFile(); DeleteOldValues(); } @@ -480,7 +405,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..208692c97 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -183,12 +183,6 @@ namespace NzbDrone.Core.Configuration public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty); - public bool TrustCgnatIpAddresses - { - get { return GetValueBoolean("TrustCgnatIpAddresses", false); } - set { SetValue("TrustCgnatIpAddresses", value); } - } - private string GetValue(string key) { return GetValue(key, string.Empty); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 796e277b7..dc76a5a31 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -254,7 +254,7 @@ namespace NzbDrone.Core.Datastore protected void Delete(SqlBuilder builder) { - var sql = builder.AddDeleteTemplate(typeof(TModel)); + 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/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..c592d17f2 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -2,7 +2,6 @@ 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; @@ -61,22 +60,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 +90,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 +107,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 +135,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 +162,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 +186,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/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..d0a661751 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteSchemaDumper.cs @@ -219,7 +219,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework protected virtual IList ReadTables() { - const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' 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(); diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 637f61b99..aa87b40f2 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -91,9 +91,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 +110,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 +124,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..ae076c14b 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -7,7 +7,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.Model; using NzbDrone.Core.Validation; @@ -26,9 +25,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2 ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } 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..f8fd44e97 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.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.Model; namespace NzbDrone.Core.Download.Clients.Blackhole @@ -21,9 +20,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { } 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..ca63ba0e7 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -9,7 +9,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.Model; using NzbDrone.Core.Validation; @@ -24,9 +23,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } 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/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..09db5a136 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -10,7 +10,6 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -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, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, 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) { 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); @@ -80,7 +76,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { 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)); @@ -221,7 +217,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 +268,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,15 +305,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 AddFromTorrentLink(TorrentInfo release, string hash, string torrentLink) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index e78f5f5d2..1759cf600 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); @@ -276,15 +271,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..73c6de8a2 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.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; @@ -23,9 +21,8 @@ namespace NzbDrone.Core.Download.Clients.Flood ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } @@ -59,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.Flood } } - return result.Where(t => t.IsNotNullOrWhiteSpace()); + return result; } public override string Name => "Flood"; diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs index 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/FreeboxDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs index effdf37d9..ee0fff8e1 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs @@ -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(9, 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..cec808592 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -6,7 +6,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.Model; namespace NzbDrone.Core.Download.Clients.FreeboxDownload @@ -20,9 +19,8 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 560c40eb3..b5aa1eb06 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -6,7 +6,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.Model; using NzbDrone.Core.Validation; @@ -21,9 +20,8 @@ namespace NzbDrone.Core.Download.Clients.Hadouken ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } 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/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..b286bf922 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; } 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..6555ce6cc 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.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.Model; using NzbDrone.Core.Validation; @@ -31,9 +30,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent IConfigService configService, IDiskProvider diskProvider, ICacheManager cacheManager, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxySelector = proxySelector; 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/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 33fcfc5ca..14bee89fc 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -25,6 +25,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); } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index 7c9cc8768..cdf161c7f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -146,7 +146,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("paused", false); } - else if ((QBittorrentState)settings.InitialState == QBittorrentState.Stop) + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) { request.AddFormParameter("paused", true); } @@ -176,7 +176,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 +212,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5 - if (ex.InnerException is HttpException 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 +254,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (DownloadClientException ex) { // qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled - if (ex.InnerException is HttpException httpException && httpException.Response.StatusCode == HttpStatusCode.Forbidden) + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden) { return; } @@ -263,6 +263,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") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index cfa7c9934..5e967d095 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -246,20 +246,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", category); } - // Avoid extraneous API version check if initial state is ForceStart - if ((QBittorrentState)settings.InitialState is QBittorrentState.Start or QBittorrentState.Stop) + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) { - 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); - } + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + { + request.AddFormParameter("paused", true); } if (settings.SequentialOrder) @@ -271,15 +265,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { request.AddFormParameter("firstLastPiecePrio", true); } - - if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Original) - { - request.AddFormParameter("contentLayout", "Original"); - } - else if ((QBittorrentContentLayout)settings.ContentLayout == QBittorrentContentLayout.Subfolder) - { - request.AddFormParameter("contentLayout", "Subfolder"); - } } public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) @@ -297,7 +282,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 +304,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 +313,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") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 8d157dc30..4427472ae 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,21 @@ 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")] + [FieldDefinition(9, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")] public bool SequentialOrder { get; set; } - [FieldDefinition(10, Label = "DownloadClientQbittorrentSettingsFirstAndLastFirst", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText")] + [FieldDefinition(10, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")] 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/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/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..a2bd2bf91 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -5,7 +5,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Transmission { @@ -16,9 +15,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(proxy, torrentFileInfoReader, seedConfigProvider, 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..6e723a32a 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -6,7 +6,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.Model; using NzbDrone.Core.Validation; @@ -21,19 +20,15 @@ namespace NzbDrone.Core.Download.Clients.Transmission ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } protected override string AddFromMagnetLink(TorrentInfo release, string hash, string magnetLink) { - var category = GetCategoryForRelease(release) ?? Settings.Category; - var downloadDirectory = GetDownloadDirectory(category); - - _proxy.AddTorrentFromUrl(magnetLink, downloadDirectory, Settings); + _proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings); _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); if (Settings.Priority == (int)TransmissionPriority.First) @@ -46,10 +41,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission protected override string AddFromTorrentFile(TorrentInfo release, string hash, string filename, byte[] fileContent) { - var category = GetCategoryForRelease(release) ?? Settings.Category; - var downloadDirectory = GetDownloadDirectory(category); - - _proxy.AddTorrentFromData(fileContent, downloadDirectory, Settings); + _proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings); _proxy.SetTorrentSeedingConfiguration(hash, release.SeedConfiguration, Settings); if (Settings.Priority == (int)TransmissionPriority.First) @@ -81,14 +73,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 +88,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/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index 76f8684e0..2c2334f44 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -4,7 +4,6 @@ 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; @@ -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/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/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index 3b87962bb..79f5df0e4 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -4,7 +4,6 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Transmission; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; namespace NzbDrone.Core.Download.Clients.Vuze { @@ -17,9 +16,8 @@ namespace NzbDrone.Core.Download.Clients.Vuze ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(proxy, torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 628ebdf52..5218f9ebe 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -10,7 +10,6 @@ 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; @@ -28,9 +27,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent IConfigService configService, IDiskProvider diskProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 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..ef12042fb 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -7,9 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.UTorrent @@ -23,9 +21,8 @@ namespace NzbDrone.Core.Download.Clients.UTorrent ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, localizationService, logger) + : base(torrentFileInfoReader, seedConfigProvider, configService, diskProvider, logger) { _proxy = proxy; } @@ -75,9 +72,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) 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/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 9a7da6fd6..89fce8f3c 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.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.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -20,7 +19,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; } @@ -29,7 +27,20 @@ namespace NzbDrone.Core.Download public virtual ProviderMessage Message => null; - public IEnumerable DefaultDefinitions => new List(); + public IEnumerable DefaultDefinitions + { + get + { + var config = (IProviderConfig)new TSettings(); + + yield return new DownloadClientDefinition + { + Name = GetType().Name, + Implementation = GetType().Name, + Settings = config + }; + } + } public ProviderDefinition Definition { get; set; } @@ -42,12 +53,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/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 19aedf751..d692d36ee 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -57,11 +57,11 @@ 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) + if (blockedIndexers.TryGetValue(client.Definition.Id, out var downloadClientStatus)) { _logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime()); continue; @@ -75,19 +75,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/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 223c24384..d5e6368f1 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; @@ -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,6 +43,7 @@ namespace NzbDrone.Core.Download _downloadClientStatusService = downloadClientStatusService; _indexerFactory = indexerFactory; _indexerStatusService = indexerStatusService; + _rateLimitService = rateLimitService; _eventAggregator = eventAggregator; _logger = logger; } @@ -69,7 +74,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 +131,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 +150,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 +204,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/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index 217e70a31..d9d7f7133 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -8,7 +8,6 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -25,9 +24,8 @@ namespace NzbDrone.Core.Download ISeedConfigProvider seedConfigProvider, IConfigService configService, IDiskProvider diskProvider, - ILocalizationService localizationService, Logger logger) - : base(configService, diskProvider, localizationService, logger) + : base(configService, diskProvider, logger) { _torrentFileInfoReader = torrentFileInfoReader; _seedConfigProvider = seedConfigProvider; @@ -129,8 +127,9 @@ namespace NzbDrone.Core.Download private async Task DownloadFromWebUrl(TorrentInfo 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 @@ -172,7 +171,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..37a775e96 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -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); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs index d3198b111..e65fe0972 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using NLog; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Lifecycle; @@ -29,7 +28,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { _logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength); - return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary { { "length", MinimumLength } }), "#invalid-api-key"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), 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..48fafadae 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 { 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/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..758259a13 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; @@ -18,7 +17,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; @@ -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 69% rename from src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyStatusCheck.cs rename to src/NzbDrone.Core/HealthCheck/Checks/IndexerProxyCheck.cs index 39aeac49e..484f2bc19 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) { @@ -38,17 +37,15 @@ namespace NzbDrone.Core.HealthCheck.Checks { 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..968cb5e8f 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; @@ -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..73d217c25 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.All(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..f92e29b80 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,31 +63,14 @@ 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)) + if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null) { - var latestAvailable = _checkUpdateService.AvailableUpdate(); - - if (latestAvailable != null) - { - return new HealthCheck(GetType(), - BuildInfo.BuildDateTime.Before(DateTime.UtcNow.AddDays(-180)) ? HealthCheckResult.Error : HealthCheckResult.Warning, - _localizationService.GetLocalizedString("UpdateAvailableHealthCheckMessage", new Dictionary - { - { "version", $"v{latestAvailable.Version}" } - }), - "#new-update-is-available"); - } + return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new-update-is-available"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index a9a89dc48..78e60e314 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,38 @@ 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) { - var previous = _healthCheckResults.Find(result.Source.Name); - - if (previous != null) - { - _eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod)); - } - - _healthCheckResults.Remove(result.Source.Name); + _eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod)); } - 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 +116,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 +144,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; @@ -216,16 +176,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..8dcc1861d 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; @@ -206,27 +205,6 @@ namespace NzbDrone.Core.History history.Data.Add("GrabTitle", 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 +218,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/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/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/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/HttpIndexerProxyBase.cs b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs index d7c9acc23..b82251d6e 100644 --- a/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs +++ b/src/NzbDrone.Core/IndexerProxies/HttpIndexerProxyBase.cs @@ -41,14 +41,14 @@ namespace NzbDrone.Core.IndexerProxies // We only care about 400 responses, other error codes can be ignored if (response.StatusCode == HttpStatusCode.BadRequest) { - _logger.Error("Proxy 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/IndexerProxyBase.cs b/src/NzbDrone.Core/IndexerProxies/IndexerProxyBase.cs index 1efe8437c..dc3963875 100644 --- a/src/NzbDrone.Core/IndexerProxies/IndexerProxyBase.cs +++ b/src/NzbDrone.Core/IndexerProxies/IndexerProxyBase.cs @@ -14,7 +14,20 @@ namespace NzbDrone.Core.IndexerProxies public Type ConfigContract => typeof(TSettings); - public IEnumerable DefaultDefinitions => new List(); + public IEnumerable DefaultDefinitions + { + get + { + var config = (IProviderConfig)new TSettings(); + + yield return new IndexerProxyDefinition + { + Name = GetType().Name, + Implementation = GetType().Name, + Settings = config + }; + } + } public ProviderDefinition Definition { get; set; } public abstract ValidationResult Test(); 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..8fb138fcf 100644 --- a/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs +++ b/src/NzbDrone.Core/IndexerSearch/NewznabRequest.cs @@ -5,7 +5,7 @@ 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 TvRegex = new (@"\{((?:imdbid\:)(?[^{]+)|(?:rid\:)(?[^{]+)|(?:tvdbid\:)(?[^{]+)|(?:tmdbid\:)(?[^{]+)|(?: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\:)(?