Compare commits

..

1 Commits

Author SHA1 Message Date
Michon van Dooren 6a556a89aa Fixed: Include preferred size in quality definition reset
(cherry picked from commit 8e925ac76d2f46cf5fef1ea62a20ae5e85d3000e)
2023-07-30 16:46:46 +00:00
290 changed files with 2574 additions and 5932 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
# Explicitly set bash scripts to have unix endings # Explicitly set bash scripts to have unix endings
*.sh text eol=lf *.sh text eol=lf
distribution/osx/Readarr text eol=lf distribution/debian/* text eol=lf
macOS/Readarr text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp
+1
View File
@@ -30,6 +30,7 @@ Note that only one type of a given book is supported. If you want both an audiob
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/readarr)
Note: GitHub Issues are for Bugs and Feature Requests Only Note: GitHub Issues are for Bugs and Feature Requests Only
+141 -294
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.5' majorVersion: '0.3.1'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413' dotnetVersion: '6.0.408'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -27,10 +27,6 @@ trigger:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Readarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -38,37 +34,82 @@ pr:
- develop - develop
paths: paths:
exclude: exclude:
- .github
- src/NzbDrone.Core/Localization/Core - src/NzbDrone.Core/Localization/Core
- src/Readarr.Api.*/openapi.json
stages: stages:
- stage: Setup
displayName: Setup - stage: Build_Backend_Windows
displayName: Build Backend
dependsOn: []
jobs: jobs:
- job: - job: Backend
displayName: Build Variables strategy:
matrix:
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: $(imageName)
variables:
# Disable stylecop here - linting errors get caught by the analyze task
EnableAnalyzers: $(enableAnalysis)
steps: steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here: # Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$READARRVERSION" - bash: echo "##vso[build.updatebuildnumber]$READARRVERSION"
displayName: Set Build Name displayName: Set Build Name
- checkout: self
submodules: true
fetchDepth: 1
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- bash: | - bash: |
if [[ $BUILD_REASON == "PullRequest" ]]; then SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)" BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
echo $? > not_backend_update
else if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo 0 > not_backend_update sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
cat not_backend_update displayName: Extra Platform Support
displayName: Check for Backend File Changes - task: Cache@2
- publish: not_backend_update inputs:
artifact: not_backend_update key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
displayName: Publish update type path: $(nugetCacheFolder)
- stage: Build_Backend displayName: Cache NuGet packages
displayName: Build Backend - bash: ./build.sh --backend --enable-bsd
dependsOn: Setup displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output
- publish: $(outputFolder)
artifact: '$(osName)Backend'
displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
- stage: Build_Backend_Other
displayName: Build Backend (Other OS)
dependsOn: []
jobs: jobs:
- job: Backend - job: Backend
strategy: strategy:
@@ -81,10 +122,6 @@ stages:
osName: 'Mac' osName: 'Mac'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
enableAnalysis: 'false' enableAnalysis: 'false'
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
@@ -100,17 +137,22 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
echo $BUNDLEDVERSIONS BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled" if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
displayName: Enable Extra Platform Support displayName: Extra Platform Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-extra-platforms - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
@@ -118,38 +160,10 @@ stages:
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows')) condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- publish: $(outputFolder)
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
displayName: Frontend displayName: Frontend
dependsOn: Setup dependsOn: []
jobs: jobs:
- job: Build - job: Build
strategy: strategy:
@@ -178,6 +192,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --frontend - bash: ./build.sh --frontend
@@ -189,10 +204,10 @@ stages:
artifact: '$(osName)Frontend' artifact: '$(osName)Frontend'
displayName: Publish Frontend displayName: Publish Frontend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Installer - stage: Installer
dependsOn: dependsOn:
- Build_Backend - Build_Backend_Windows
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Windows_Installer - job: Windows_Installer
@@ -216,8 +231,8 @@ stages:
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: | - bash: |
./build.sh --packages --installer ./build.sh --packages --installer
cp distribution/windows/setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
cp distribution/windows/setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller' artifact: 'WindowsInstaller'
@@ -225,7 +240,7 @@ stages:
- stage: Packages - stage: Packages
dependsOn: dependsOn:
- Build_Backend - Build_Backend_Windows
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Other_Packages - job: Other_Packages
@@ -391,29 +406,14 @@ stages:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr) SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg) SENTRY_ORG: $(sentryOrg)
SENTRY_URL: $(sentryUrl) SENTRY_URL: $(sentryUrl)
- stage: Unit_Test - stage: Unit_Test
displayName: Unit Tests displayName: Unit Tests
dependsOn: Build_Backend dependsOn: Build_Backend_Windows
condition: succeeded()
jobs: jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Unit - job: Unit
displayName: Unit Native displayName: Unit Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
@@ -479,8 +479,6 @@ stages:
- job: Unit_Docker - job: Unit_Docker
displayName: Unit Docker displayName: Unit Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -494,11 +492,11 @@ stages:
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -532,14 +530,12 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres14 - job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres14 Database displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests artifactName: LinuxCoreTests
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432' Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr' Readarr__Postgres__User: 'readarr'
@@ -549,7 +545,7 @@ stages:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -560,7 +556,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: $(artifactName) artifactName: 'linux-x64-Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \; - bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable displayName: Make Test Dummy Executable
@@ -583,84 +579,15 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres14 Unit Tests' testRunTitle: 'LinuxCore Postgres 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: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
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 "Readarr.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=readarr \
-e POSTGRES_USER=readarr \
-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'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
dependsOn: Packages dependsOn: Packages
jobs: jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Integration_Native - job: Integration_Native
displayName: Integration Native displayName: Integration Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
MacCore: MacCore:
@@ -681,7 +608,7 @@ stages:
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -703,7 +630,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -722,10 +649,8 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres14 - job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres14 Database displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
@@ -757,7 +682,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -780,77 +705,12 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests' testRunTitle: 'Integration LinuxCore Postgres 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: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
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/Readarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-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'
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
variables: variables:
@@ -895,8 +755,6 @@ stages:
- job: Integration_Docker - job: Integration_Docker
displayName: Integration Docker displayName: Integration Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -915,7 +773,7 @@ stages:
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 15 timeoutInMinutes: 15
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -943,7 +801,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -965,7 +823,7 @@ stages:
- stage: Automation - stage: Automation
displayName: Automation displayName: Automation
dependsOn: Packages dependsOn: Packages
jobs: jobs:
- job: Automation - job: Automation
strategy: strategy:
@@ -975,23 +833,20 @@ stages:
artifactName: 'linux-x64' artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64' artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64' artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
failBuild: true
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -1013,7 +868,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -1033,35 +888,20 @@ stages:
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots' TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots - publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots' artifact: '$(osName)AutomationScreenshots'
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1')) condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
displayName: Publish Screenshot Bundle
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests' testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: $(failBuild) failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- stage: Analyze - stage: Analyze
dependsOn: dependsOn: []
- Setup
displayName: Analyze displayName: Analyze
jobs: jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Lint_Frontend - job: Lint_Frontend
displayName: Lint Frontend displayName: Lint Frontend
strategy: strategy:
@@ -1087,6 +927,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --lint - bash: ./build.sh --lint
@@ -1115,16 +956,11 @@ stages:
cliProjectVersion: '$(readarrVersion)' cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
dependsOn: Prepare
condition: | condition: |
and and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
)
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
@@ -1137,7 +973,7 @@ stages:
- checkout: self - checkout: self
submodules: true submodules: true
persistCredentials: true persistCredentials: true
fetchDepth: 1 fetchDepth: 1
- bash: ./docs.sh Windows - bash: ./docs.sh Windows
displayName: Create openapi.json displayName: Create openapi.json
- bash: | - bash: |
@@ -1145,9 +981,10 @@ stages:
git config --global user.name "Servarr" git config --global user.name "Servarr"
git checkout -b api-docs git checkout -b api-docs
git add . git add .
if git status | grep -q modified git status
if git status | grep modified
then 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 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/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else else
@@ -1171,25 +1008,33 @@ stages:
- job: Analyze_Backend - job: Analyze_Backend
displayName: Backend displayName: Backend
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
disable.coverage.autogenerate: 'true' disable.coverage.autogenerate: 'true'
EnableAnalyzers: 'false'
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core 2.1'
inputs:
version: 2.1.815
- task: UseDotNet@2
displayName: 'Install .net core 3.1'
inputs:
version: 3.1.413
- task: UseDotNet@2
displayName: 'Install .net core 5.0'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - task: Cache@2
displayName: Enable Windows Test Service inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
@@ -1200,14 +1045,16 @@ stages:
projectName: 'Readarr' projectName: 'Readarr'
projectVersion: '$(readarrVersion)' projectVersion: '$(readarrVersion)'
extraProperties: | extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/** sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,./src/Libraries/**
sonar.coverage.exclusions=**/Readarr.Api.V1/**/* sonar.coverage.exclusions=**/Readarr.Api.V1/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f net6.0 -r win-x64 ./build.sh --backend -f net6.0 -r linux-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
@@ -1230,6 +1077,7 @@ stages:
- Unit_Test - Unit_Test
- Integration - Integration
- Automation - Automation
- Build_Backend_Other
condition: eq(variables['system.pullrequest.isfork'], false) condition: eq(variables['system.pullrequest.isfork'], false)
displayName: Build Status Report displayName: Build Status Report
jobs: jobs:
@@ -1253,4 +1101,3 @@ stages:
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId) DISCORDTHREADID: $(discordThreadId)
+10 -9
View File
@@ -23,7 +23,7 @@ UpdateVersionNumber()
echo "Updating Version Info" echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" distribution/osx/Readarr.app/Contents/Info.plist sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" macOS/Readarr.app/Contents/Info.plist
fi fi
} }
@@ -183,7 +183,7 @@ PackageMacOSApp()
rm -rf $folder rm -rf $folder
mkdir -p $folder mkdir -p $folder
cp -r distribution/osx/Readarr.app $folder cp -r macOS/Readarr.app $folder
mkdir -p $folder/Readarr.app/Contents/MacOS mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries" echo "Copying Binaries"
@@ -245,7 +245,7 @@ BuildInstaller()
local framework="$1" local framework="$1"
local runtime="$2" local runtime="$2"
./_inno/ISCC.exe distribution/windows/setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime" ./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
} }
InstallInno() InstallInno()
@@ -391,21 +391,22 @@ then
fi fi
fi fi
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]]; if [ "$FRONTEND" = "YES" ];
then then
YarnInstall YarnInstall
RunWebpack
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
then then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI LintUI
fi fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ]; if [ "$PACKAGES" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [ plugins: [
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }], ['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }], ['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
// Stage 2 // Stage 2
'@babel/plugin-transform-export-namespace-from', '@babel/plugin-proposal-export-namespace-from',
// Stage 3 // Stage 3
['@babel/plugin-transform-class-properties', { loose }], ['@babel/plugin-proposal-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import' '@babel/plugin-syntax-dynamic-import'
], ],
env: { env: {
+2 -3
View File
@@ -36,7 +36,7 @@ module.exports = (env) => {
}, },
entry: { entry: {
index: 'index.ts' index: 'index.js'
}, },
resolve: { resolve: {
@@ -98,8 +98,7 @@ module.exports = (env) => {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs', template: 'frontend/src/index.ejs',
filename: 'index.html', filename: 'index.html',
publicPath: '/', publicPath: '/'
inject: false
}), }),
new FileManagerPlugin({ new FileManagerPlugin({
+4 -3
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history, hasTranslationsError }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme>
<PageConnector> <PageConnector hasTranslationsError={hasTranslationsError}>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme> </ApplyTheme>
@@ -25,7 +25,8 @@ function App({ store, history }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
}; };
export default App; export default App;
@@ -1,7 +1,6 @@
.version { .version {
margin: 0 3px; margin: 0 3px;
font-weight: bold; font-weight: bold;
font-family: var(--defaultFontFamily);
} }
.maintenance { .maintenance {
+8 -7
View File
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@@ -65,12 +64,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AppUpdated', { appName: 'Readarr' })} Readarr Updated
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Readarr', version })} blockClassName={styles.version} /> Version <span className={styles.version}>{version}</span> of Readarr has been installed, in order to get the latest changes you'll need to reload Readarr.
</div> </div>
{ {
@@ -78,14 +77,16 @@ function AppUpdatedModalContent(props) {
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div> <div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
} }
{ {
!!update.changes && !!update.changes &&
<div> <div>
<div className={styles.changes}> <div className={styles.changes}>
{translate('WhatsNew')} What's new?
</div> </div>
<UpdateChanges <UpdateChanges
@@ -112,14 +113,14 @@ function AppUpdatedModalContent(props) {
<Button <Button
onPress={onSeeChangesPress} onPress={onSeeChangesPress}
> >
{translate('RecentChanges')} Recent Changes
</Button> </Button>
<Button <Button
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Reload')} Reload
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+4 -5
View File
@@ -7,7 +7,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css'; import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) { function ConnectionLostModal(props) {
@@ -23,16 +22,16 @@ function ConnectionLostModal(props) {
> >
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('ConnectionLost')} Connection Lost
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
{translate('ConnectionLostToBackend', { appName: 'Readarr' })} Readarr has lost its connection to the backend and will need to be reloaded to restore functionality.
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Readarr' })} Readarr will try to connect automatically, or you can click reload below.
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@@ -40,7 +39,7 @@ function ConnectionLostModal(props) {
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Reload')} Reload
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'), createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR), createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR), createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
( (
author, author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState dimensionsState
) => { ) => {
return { return {
...author, ...author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -14,39 +14,14 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ const nameOptions = [
{ { key: 'firstLast', value: translate('NameFirstLast') },
key: 'firstLast', { key: 'lastFirst', value: translate('NameLastFirst') }
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
const posterSizeOptions = [ const posterSizeOptions = [
{ { key: 'small', value: 'Small' },
key: 'small', { key: 'medium', value: 'Medium' },
get value() { { key: 'large', value: 'Large' }
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
class AuthorIndexOverviewOptionsModalContent extends Component { class AuthorIndexOverviewOptionsModalContent extends Component {
@@ -14,45 +14,15 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const posterSizeOptions = [ const posterSizeOptions = [
{ { key: 'small', value: 'Small' },
key: 'small', { key: 'medium', value: 'Medium' },
get value() { { key: 'large', value: 'Large' }
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
const nameOptions = [ const nameOptions = [
{ { key: 'no', value: translate('NoName') },
key: 'no', { key: 'firstLast', value: translate('NameFirstLast') },
get value() { { key: 'lastFirst', value: translate('NameLastFirst') }
return translate('NoName');
}
},
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
class AuthorIndexPosterOptionsModalContent extends Component { class AuthorIndexPosterOptionsModalContent extends Component {
@@ -7,18 +7,8 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ const nameOptions = [
{ { key: 'firstLast', value: translate('NameFirstLast') },
key: 'firstLast', { key: 'lastFirst', value: translate('NameLastFirst') }
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
class AuthorIndexTableOptions extends Component { class AuthorIndexTableOptions extends Component {
@@ -6,5 +6,4 @@
.statusIcon { .statusIcon {
width: 20px !important; width: 20px !important;
text-align: center;
} }
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'), createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK), createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH), createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH), createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent'; import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
function BookInteractiveSearchModal(props) { function BookInteractiveSearchModal(props) {
@@ -15,7 +14,6 @@ function BookInteractiveSearchModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
+1 -1
View File
@@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{translate('CountAuthorsSelected', { selectedCount })} {selectedCount} Author(s) Selected
</div> </div>
<SpinnerButton <SpinnerButton
@@ -25,10 +25,6 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.image { .image {
height: 250px; height: 250px;
@@ -6,7 +6,6 @@ interface CssExports {
'image': string; 'image': string;
'imageContainer': string; 'imageContainer': string;
'message': string; 'message': string;
'version': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;
@@ -1,77 +0,0 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;
@@ -206,11 +206,9 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp; const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => { const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return { return {
key: name, key: availablePropFilter.name,
value: typeof label === 'function' ? label() : label value: availablePropFilter.label
}; };
}).sort((a, b) => a.value.localeCompare(b.value)); }).sort((a, b) => a.value.localeCompare(b.value));
@@ -9,10 +9,6 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { value } = this.props; const { value } = this.props;
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) { if (value !== prevProps.value && !this.state.isFocused) {
this.setState({ this.setState({
value: value == null ? '' : value.toString() value: value == null ? '' : value.toString()
}); });
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key} value={key}
{...otherOptionProps} {...otherOptionProps}
> >
{typeof optionValue === 'function' ? optionValue() : optionValue} {optionValue}
</option> </option>
); );
}) })
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return ( return (
<span <span
className={containerClassName} className={containerClassName}
title={typeof title === 'function' ? title() : title} title={title}
> >
{icon} {icon}
</span> </span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired, name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), title: PropTypes.string,
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired fixedWidth: PropTypes.bool.isRequired
}; };
@@ -97,7 +97,6 @@ class SpinnerErrorButton extends Component {
render() { render() {
const { const {
kind,
isSpinning, isSpinning,
error, error,
children, children,
@@ -113,7 +112,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError; const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK; let iconName = icons.CHECK;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS; let iconKind = kinds.SUCCESS;
if (hasWarning) { if (hasWarning) {
iconName = icons.WARNING; iconName = icons.WARNING;
@@ -127,7 +126,6 @@ class SpinnerErrorButton extends Component {
return ( return (
<SpinnerButton <SpinnerButton
kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
@@ -156,7 +154,6 @@ class SpinnerErrorButton extends Component {
} }
SpinnerErrorButton.propTypes = { SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
@@ -10,8 +10,7 @@ class InlineMarkdown extends Component {
render() { render() {
const { const {
className, className,
data, data
blockClassName
} = this.props; } = this.props;
// For now only replace links or code blocks (not both) // For now only replace links or code blocks (not both)
@@ -48,7 +47,7 @@ class InlineMarkdown extends Component {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
} }
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>); markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
} }
@@ -67,8 +66,7 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = { InlineMarkdown.propTypes = {
className: PropTypes.string, className: PropTypes.string,
data: PropTypes.string, data: PropTypes.string
blockClassName: PropTypes.string
}; };
export default InlineMarkdown; export default InlineMarkdown;
@@ -32,7 +32,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
{typeof filter.label === 'function' ? filter.label() : filter.label} {filter.label}
</FilterMenuItem> </FilterMenuItem>
); );
}) })
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const { const {
version, version,
isLocalStorageSupported, isLocalStorageSupported,
translationsError, hasTranslationsError,
authorError, authorError,
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) { if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (translationsError) { } else if (hasTranslationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API'); errorMessage = 'Failed to load translations from API';
} else if (authorError) { } else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API'); errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) { } else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = { ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
translationsError: PropTypes.object, hasTranslationsError: PropTypes.bool.isRequired,
authorError: PropTypes.object, authorError: PropTypes.object,
customFiltersError: PropTypes.object, customFiltersError: PropTypes.object,
tagsError: PropTypes.object, tagsError: PropTypes.object,
+10 -20
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -52,7 +52,6 @@ const selectIsPopulated = createSelector(
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
( (
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
@@ -61,8 +60,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated
translationsIsPopulated
) => { ) => {
return ( return (
customFiltersIsPopulated && customFiltersIsPopulated &&
@@ -72,8 +70,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated
translationsIsPopulated
); );
} }
); );
@@ -87,7 +84,6 @@ const selectErrors = createSelector(
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error,
( (
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -96,8 +92,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError, systemStatusError
translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
customFiltersError || customFiltersError ||
@@ -107,8 +102,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
systemStatusError || systemStatusError
translationsError
); );
return { return {
@@ -120,8 +114,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError, systemStatusError
translationsError
}; };
} }
); );
@@ -183,9 +176,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() { dispatchFetchStatus() {
dispatch(fetchStatus()); dispatch(fetchStatus());
}, },
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) { onResize(dimensions) {
dispatch(saveDimensions(dimensions)); dispatch(saveDimensions(dimensions));
}, },
@@ -220,7 +210,6 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
} }
} }
@@ -236,6 +225,7 @@ class PageConnector extends Component {
render() { render() {
const { const {
hasTranslationsError,
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchAuthor, dispatchFetchAuthor,
@@ -247,15 +237,15 @@ class PageConnector extends Component {
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps ...otherProps
} = this.props; } = this.props;
if (hasError || !this.state.isLocalStorageSupported) { if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
return ( return (
<ErrorPage <ErrorPage
{...this.state} {...this.state}
{...otherProps} {...otherProps}
hasTranslationsError={hasTranslationsError}
/> />
); );
} }
@@ -276,6 +266,7 @@ class PageConnector extends Component {
} }
PageConnector.propTypes = { PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
@@ -289,7 +280,6 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
}; };
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.AUTHOR_CONTINUING, iconName: icons.AUTHOR_CONTINUING,
title: () => translate('Library'), title: 'Library',
to: '/', to: '/',
alias: '/authors', alias: '/authors',
children: [ children: [
{ {
title: () => translate('Authors'), title: 'Authors',
to: '/authors' to: '/authors'
}, },
{ {
title: () => translate('Books'), title: 'Books',
to: '/books' to: '/books'
}, },
{ {
title: () => translate('AddNew'), title: 'Add New',
to: '/add/search' to: '/add/search'
}, },
{ {
title: () => translate('Bookshelf'), title: 'Bookshelf',
to: '/shelf' to: '/shelf'
}, },
{ {
title: () => translate('UnmappedFiles'), title: 'Unmapped Files',
to: '/unmapped' to: '/unmapped'
} }
] ]
@@ -50,26 +50,26 @@ const links = [
{ {
iconName: icons.CALENDAR, iconName: icons.CALENDAR,
title: () => translate('Calendar'), title: 'Calendar',
to: '/calendar' to: '/calendar'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: () => translate('Activity'), title: 'Activity',
to: '/activity/queue', to: '/activity/queue',
children: [ children: [
{ {
title: () => translate('Queue'), title: 'Queue',
to: '/activity/queue', to: '/activity/queue',
statusComponent: QueueStatusConnector statusComponent: QueueStatusConnector
}, },
{ {
title: () => translate('History'), title: 'History',
to: '/activity/history' to: '/activity/history'
}, },
{ {
title: () => translate('Blocklist'), title: 'Blocklist',
to: '/activity/blocklist' to: '/activity/blocklist'
} }
] ]
@@ -77,15 +77,15 @@ const links = [
{ {
iconName: icons.WARNING, iconName: icons.WARNING,
title: () => translate('Wanted'), title: 'Wanted',
to: '/wanted/missing', to: '/wanted/missing',
children: [ children: [
{ {
title: () => translate('Missing'), title: 'Missing',
to: '/wanted/missing' to: '/wanted/missing'
}, },
{ {
title: () => translate('CutoffUnmet'), title: 'Cutoff Unmet',
to: '/wanted/cutoffunmet' to: '/wanted/cutoffunmet'
} }
] ]
@@ -93,55 +93,55 @@ const links = [
{ {
iconName: icons.SETTINGS, iconName: icons.SETTINGS,
title: () => translate('Settings'), title: 'Settings',
to: '/settings', to: '/settings',
children: [ children: [
{ {
title: () => translate('MediaManagement'), title: 'Media Management',
to: '/settings/mediamanagement' to: '/settings/mediamanagement'
}, },
{ {
title: () => translate('Profiles'), title: 'Profiles',
to: '/settings/profiles' to: '/settings/profiles'
}, },
{ {
title: () => translate('Quality'), title: 'Quality',
to: '/settings/quality' to: '/settings/quality'
}, },
{ {
title: () => translate('CustomFormats'), title: translate('CustomFormats'),
to: '/settings/customformats' to: '/settings/customformats'
}, },
{ {
title: () => translate('Indexers'), title: translate('Indexers'),
to: '/settings/indexers' to: '/settings/indexers'
}, },
{ {
title: () => translate('DownloadClients'), title: 'Download Clients',
to: '/settings/downloadclients' to: '/settings/downloadclients'
}, },
{ {
title: () => translate('ImportLists'), title: 'Import Lists',
to: '/settings/importlists' to: '/settings/importlists'
}, },
{ {
title: () => translate('Connect'), title: 'Connect',
to: '/settings/connect' to: '/settings/connect'
}, },
{ {
title: () => translate('Metadata'), title: 'Metadata',
to: '/settings/metadata' to: '/settings/metadata'
}, },
{ {
title: () => translate('Tags'), title: 'Tags',
to: '/settings/tags' to: '/settings/tags'
}, },
{ {
title: () => translate('General'), title: 'General',
to: '/settings/general' to: '/settings/general'
}, },
{ {
title: () => translate('Ui'), title: 'UI',
to: '/settings/ui' to: '/settings/ui'
} }
] ]
@@ -149,32 +149,32 @@ const links = [
{ {
iconName: icons.SYSTEM, iconName: icons.SYSTEM,
title: () => translate('System'), title: 'System',
to: '/system/status', to: '/system/status',
children: [ children: [
{ {
title: () => translate('Status'), title: 'Status',
to: '/system/status', to: '/system/status',
statusComponent: HealthStatusConnector statusComponent: HealthStatusConnector
}, },
{ {
title: () => translate('Tasks'), title: 'Tasks',
to: '/system/tasks' to: '/system/tasks'
}, },
{ {
title: () => translate('Backup'), title: 'Backup',
to: '/system/backup' to: '/system/backup'
}, },
{ {
title: () => translate('Updates'), title: 'Updates',
to: '/system/updates' to: '/system/updates'
}, },
{ {
title: () => translate('Events'), title: 'Events',
to: '/system/events' to: '/system/events'
}, },
{ {
title: () => translate('LogFiles'), title: 'Log Files',
to: '/system/logs/files' to: '/system/logs/files'
} }
] ]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
} }
<span className={isChildItem ? styles.noIcon : null}> <span className={isChildItem ? styles.noIcon : null}>
{typeof title === 'function' ? title() : title} {title}
</span> </span>
{ {
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = { PageSidebarItem.propTypes = {
iconName: PropTypes.object, iconName: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, title: PropTypes.string.isRequired,
to: PropTypes.string.isRequired, to: PropTypes.string.isRequired,
isActive: PropTypes.bool, isActive: PropTypes.bool,
isActiveParent: PropTypes.bool, isActiveParent: PropTypes.bool,
+1 -3
View File
@@ -1,10 +1,8 @@
import React from 'react'; import React from 'react';
type PropertyFunction<T> = () => T;
interface Column { interface Column {
name: string; name: string;
label: string | PropertyFunction<string> | React.ReactNode; label: string | React.ReactNode;
columnLabel?: string; columnLabel?: string;
isSortable?: boolean; isSortable?: boolean;
isVisible: boolean; isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)} {...getTableHeaderCellProps(otherProps)}
{...column} {...column}
> >
{typeof column.label === 'function' ? column.label() : column.label} {column.label}
</TableHeaderCell> </TableHeaderCell>
); );
}) })
@@ -30,7 +30,6 @@ class TableHeaderCell extends Component {
const { const {
className, className,
name, name,
label,
columnLabel, columnLabel,
isSortable, isSortable,
isVisible, isVisible,
@@ -54,8 +53,7 @@ class TableHeaderCell extends Component {
{...otherProps} {...otherProps}
component="th" component="th"
className={className} className={className}
label={typeof label === 'function' ? label() : label} title={columnLabel}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress} onPress={this.onPress}
> >
{children} {children}
@@ -79,8 +77,7 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = { TableHeaderCell.propTypes = {
className: PropTypes.string, className: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]), columnLabel: PropTypes.string,
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool, isSortable: PropTypes.bool,
isVisible: PropTypes.bool, isVisible: PropTypes.bool,
isModifiable: PropTypes.bool, isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false} isDisabled={isModifiable === false}
onChange={onVisibleChange} onChange={onVisibleChange}
/> />
{typeof label === 'function' ? label() : label} {label}
</label> </label>
{ {
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = { TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn <TableOptionsColumn
name={name} name={name}
label={typeof label === 'function' ? label() : label} label={label}
isVisible={isVisible} isVisible={isVisible}
isModifiable={isModifiable} isModifiable={isModifiable}
index={index} index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = { TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
+1 -7
View File
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { tooltipPositions } from 'Helpers/Props';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import styles from './Popover.css'; import styles from './Popover.css';
@@ -31,13 +30,8 @@ function Popover(props) {
} }
Popover.propTypes = { Popover.propTypes = {
className: PropTypes.string,
bodyClassName: PropTypes.string,
anchor: PropTypes.node.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
position: PropTypes.oneOf(tooltipPositions.all),
canFlip: PropTypes.bool
}; };
export default Popover; export default Popover;
@@ -1,8 +0,0 @@
enum TooltipPosition {
Top = 'top',
Right = 'right',
Bottom = 'bottom',
Left = 'left',
}
export default TooltipPosition;
-2
View File
@@ -6,7 +6,6 @@ export const BOOKSHELF = 'bookshelf';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect'; export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@@ -35,7 +34,6 @@ export const all = [
KEY_VALUE_LIST, KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT, MONITOR_BOOKS_SELECT,
MONITOR_NEW_ITEMS_SELECT, MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
@@ -69,7 +69,7 @@ const columns = [
name: 'customFormats', name: 'customFormats',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.INTERACTIVE, name: icons.INTERACTIVE,
title: () => translate('CustomFormat') title: translate('CustomFormat')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
}; };
const importModeOptions = [ const importModeOptions = [
{ key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true }, { key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: () => translate('MoveFiles') }, { key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: () => translate('HardlinkCopyFiles') } { key: 'copy', value: translate('HardlinkCopyFiles') }
]; ];
const SELECT = 'select'; const SELECT = 'select';
@@ -1,11 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props'; import { icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchRow from './InteractiveSearchRow'; import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearch.css';
@@ -57,7 +56,7 @@ const columns = [
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: () => translate('CustomFormatScore') title: translate('CustomFormatScore')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -113,17 +112,17 @@ function InteractiveSearch(props) {
{ {
!isFetching && isPopulated && !totalReleasesCount ? !isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO}> <div className={styles.blankpad}>
{translate('NoResultsFound')} No results found
</Alert> : </div> :
null null
} }
{ {
!!totalReleasesCount && isPopulated && !items.length ? !!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING}> <div className={styles.blankpad}>
{translate('AllResultsAreHiddenByTheAppliedFilter')} All results are hidden by the applied filter
</Alert> : </div> :
null null
} }
@@ -158,7 +157,7 @@ function InteractiveSearch(props) {
{ {
totalReleasesCount !== items.length && !!items.length ? totalReleasesCount !== items.length && !!items.length ?
<div className={styles.filteredMessage}> <div className={styles.filteredMessage}>
{translate('SomeResultsAreHiddenByTheAppliedFilter')} Some results are hidden by the applied filter
</div> : </div> :
null null
} }
@@ -32,18 +32,6 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD; return icons.DOWNLOAD;
} }
function getDownloadKind(isGrabbed, grabError, downloadAllowed) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError || !downloadAllowed) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) { if (isGrabbing) {
return ''; return '';
@@ -224,7 +212,7 @@ class InteractiveSearchRow extends Component {
{ {
<SpinnerIconButton <SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)} name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)} kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)} title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing} isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress} onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { name })} message={translate('DeleteCustomFormatMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}
@@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCondition')} title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', { name })} message={translate('DeleteConditionMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen} isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteDownloadClient')} title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', { name })} message={translate('DeleteDownloadClientMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient} onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose} onCancel={this.onDeleteDownloadClientModalClose}
@@ -27,25 +27,9 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
key: NO_CHANGE, { key: 'enabled', value: translate('Enabled') },
get value() { { key: 'disabled', value: translate('Disabled') },
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageDownloadClientsEditModalContent( function ManageDownloadClientsEditModalContent(
@@ -180,7 +164,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountDownloadClientsSelected', { selectedCount })} {translate('CountDownloadClientsSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -36,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: () => translate('Implementation'), label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enable', name: 'enable',
label: () => translate('Enabled'), label: translate('Enabled'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: () => translate('Priority'), label: translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeCompletedDownloads', name: 'removeCompletedDownloads',
label: () => translate('RemoveCompleted'), label: translate('RemoveCompleted'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeFailedDownloads', name: 'removeFailedDownloads',
label: () => translate('RemoveFailed'), label: translate('RemoveFailed'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')} title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', { message={translate('DeleteSelectedDownloadClientsMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -72,24 +72,9 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ { key: 'add', value: translate('Add') },
key: 'add', { key: 'remove', value: translate('Remove') },
get value() { { key: 'replace', value: translate('Replace') },
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -1,12 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons, kinds } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
import RemotePathMapping from './RemotePathMapping'; import RemotePathMapping from './RemotePathMapping';
@@ -52,11 +50,6 @@ class RemotePathMappings extends Component {
errorMessage={translate('UnableToLoadRemotePathMappings')} errorMessage={translate('UnableToLoadRemotePathMappings')}
{...otherProps} {...otherProps}
> >
<Alert kind={kinds.INFO}>
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Readarr', wikiLink: 'https://wiki.servarr.com/readarr/settings#remote-path-mappings' })} />
</Alert>
<div className={styles.remotePathMappingsHeader}> <div className={styles.remotePathMappingsHeader}>
<div className={styles.host}> <div className={styles.host}>
{translate('Host')} {translate('Host')}
@@ -103,6 +103,7 @@ class GeneralSettings extends Component {
isResettingApiKey, isResettingApiKey,
isWindows, isWindows,
isWindowsService, isWindowsService,
isDocker,
mode, mode,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange, onInputChange,
@@ -170,6 +171,7 @@ class GeneralSettings extends Component {
settings={settings} settings={settings}
isWindows={isWindows} isWindows={isWindows}
packageUpdateMechanism={packageUpdateMechanism} packageUpdateMechanism={packageUpdateMechanism}
isDocker={isDocker}
onInputChange={onInputChange} onInputChange={onInputChange}
/> />
@@ -212,6 +214,7 @@ GeneralSettings.propTypes = {
hasSettings: PropTypes.bool.isRequired, hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isWindowsService: PropTypes.bool.isRequired, isWindowsService: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
@@ -26,6 +26,7 @@ function createMapStateToProps() {
isResettingApiKey, isResettingApiKey,
isWindows: systemStatus.isWindows, isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
isDocker: systemStatus.isDocker,
mode: systemStatus.mode, mode: systemStatus.mode,
packageUpdateMechanism: systemStatus.packageUpdateMechanism, packageUpdateMechanism: systemStatus.packageUpdateMechanism,
...sectionSettings ...sectionSettings
+25 -13
View File
@@ -8,17 +8,12 @@ import { inputTypes, sizes } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const branchValues = [
'master',
'develop',
'nightly'
];
function UpdateSettings(props) { function UpdateSettings(props) {
const { const {
advancedSettings, advancedSettings,
settings, settings,
isWindows, isWindows,
isDocker,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange onInputChange
} = props; } = props;
@@ -49,21 +44,32 @@ function UpdateSettings(props) {
updateOptions.push({ key: 'script', value: 'Script' }); updateOptions.push({ key: 'script', value: 'Script' });
if (isDocker) {
return (
<FieldSet legend={translate('Updates')}>
<div>
{translate('UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead')}
</div>
</FieldSet>
);
}
return ( return (
<FieldSet legend={translate('Updates')}> <FieldSet legend={translate('Updates')}>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('Branch')}</FormLabel> <FormLabel>
{translate('Branch')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.AUTO_COMPLETE} type={inputTypes.TEXT}
name="branch" name="branch"
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')} helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr" helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
{...branch} {...branch}
values={branchValues}
onChange={onInputChange} onChange={onInputChange}
readOnly={usingExternalUpdateMechanism} readOnly={usingExternalUpdateMechanism}
/> />
@@ -77,13 +83,14 @@ function UpdateSettings(props) {
isAdvanced={true} isAdvanced={true}
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<FormLabel>{translate('Automatic')}</FormLabel> <FormLabel>
{translate('Automatic')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="updateAutomatically" name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')} helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
onChange={onInputChange} onChange={onInputChange}
{...updateAutomatically} {...updateAutomatically}
/> />
@@ -93,7 +100,9 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('Mechanism')}</FormLabel> <FormLabel>
{translate('Mechanism')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
@@ -112,7 +121,9 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>{translate('ScriptPath')}</FormLabel> <FormLabel>
{translate('ScriptPath')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
@@ -133,6 +144,7 @@ UpdateSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };
@@ -107,7 +107,7 @@ class ImportList extends Component {
isOpen={this.state.isDeleteImportListModalOpen} isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteImportList')} title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })} message={translate('DeleteImportListMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList} onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose} onCancel={this.onDeleteImportListModalClose}
@@ -27,25 +27,9 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const autoAddOptions = [ const autoAddOptions = [
{ { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
key: NO_CHANGE, { key: 'enabled', value: translate('Enabled') },
get value() { { key: 'disabled', value: translate('Disabled') },
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageImportListsEditModalContent( function ManageImportListsEditModalContent(
@@ -184,7 +168,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountImportListsSelected', { selectedCount })} {translate('CountImportListsSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: () => translate('Implementation'), label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'qualityProfileId', name: 'qualityProfileId',
label: () => translate('QualityProfile'), label: translate('QualityProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'metadataProfileId', name: 'metadataProfileId',
label: () => translate('MetadataProfile'), label: translate('MetadataProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'rootFolderPath', name: 'rootFolderPath',
label: () => translate('RootFolder'), label: translate('RootFolder'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticAdd', name: 'enableAutomaticAdd',
label: () => translate('AutoAdd'), label: translate('AutoAdd'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: () => translate('Tags'), label: translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -283,9 +283,9 @@ function ManageImportListsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedImportLists')} title={translate('DeleteSelectedImportLists')}
message={translate('DeleteSelectedImportListsMessageText', { message={translate('DeleteSelectedImportListsMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,24 +70,9 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ { key: 'add', value: translate('Add') },
key: 'add', { key: 'remove', value: translate('Remove') },
get value() { { key: 'replace', value: translate('Replace') },
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -152,7 +152,7 @@ class Indexer extends Component {
isOpen={this.state.isDeleteIndexerModalOpen} isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexer')} title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })} message={translate('DeleteIndexerMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer} onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose} onCancel={this.onDeleteIndexerModalClose}
@@ -27,25 +27,9 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
key: NO_CHANGE, { key: 'enabled', value: translate('Enabled') },
get value() { { key: 'disabled', value: translate('Disabled') },
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageIndexersEditModalContent( function ManageIndexersEditModalContent(
@@ -178,7 +162,7 @@ function ManageIndexersEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', { selectedCount })} {translate('CountIndexersSelected', [selectedCount])}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: () => translate('Implementation'), label: translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableRss', name: 'enableRss',
label: () => translate('EnableRSS'), label: translate('EnableRSS'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticSearch', name: 'enableAutomaticSearch',
label: () => translate('EnableAutomaticSearch'), label: translate('EnableAutomaticSearch'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableInteractiveSearch', name: 'enableInteractiveSearch',
label: () => translate('EnableInteractiveSearch'), label: translate('EnableInteractiveSearch'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: () => translate('Priority'), label: translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: () => translate('Tags'), label: translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedIndexers')} title={translate('DeleteSelectedIndexers')}
message={translate('DeleteSelectedIndexersMessageText', { message={translate('DeleteSelectedIndexersMessageText', [
count: selectedIds.length, selectedIds.length,
})} ])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,24 +70,9 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ { key: 'add', value: translate('Add') },
key: 'add', { key: 'remove', value: translate('Remove') },
get value() { { key: 'replace', value: translate('Replace') },
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -95,7 +95,7 @@ class RootFolder extends Component {
isOpen={this.state.isDeleteRootFolderModalOpen} isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteRootFolder')} title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { name })} message={translate('DeleteRootFolderMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder} onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose} onCancel={this.onDeleteRootFolderModalClose}
@@ -11,51 +11,16 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [ const writeAudioTagOptions = [
{ { key: 'no', value: translate('WriteTagsNo') },
key: 'no', { key: 'sync', value: translate('WriteTagsSync') },
get value() { { key: 'allFiles', value: translate('WriteTagsAll') },
return translate('WriteTagsNo'); { key: 'newFiles', value: translate('WriteTagsNew') }
}
},
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
]; ];
const writeBookTagOptions = [ const writeBookTagOptions = [
{ { key: 'sync', value: translate('WriteTagsSync') },
key: 'sync', { key: 'allFiles', value: translate('WriteTagsAll') },
get value() { { key: 'newFiles', value: translate('WriteTagsNew') }
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
]; ];
function MetadataProvider(props) { function MetadataProvider(props) {
@@ -227,7 +227,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen} isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteNotification')} title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', { name })} message={translate('DeleteNotificationMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification} onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose} onCancel={this.onDeleteNotificationModalClose}
@@ -144,7 +144,7 @@ class MetadataProfile extends Component {
isOpen={this.state.isDeleteMetadataProfileModalOpen} isOpen={this.state.isDeleteMetadataProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteMetadataProfile')} title={translate('DeleteMetadataProfile')}
message={translate('DeleteMetadataProfileMessageText', { name })} message={translate('DeleteMetadataProfileMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteMetadataProfile} onConfirm={this.onConfirmDeleteMetadataProfile}
@@ -162,7 +162,7 @@ class QualityProfile extends Component {
isOpen={this.state.isDeleteQualityProfileModalOpen} isOpen={this.state.isDeleteQualityProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteQualityProfile')} title={translate('DeleteQualityProfile')}
message={translate('DeleteQualityProfileMessageText', { name })} message={translate('DeleteQualityProfileMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteQualityProfile} onConfirm={this.onConfirmDeleteQualityProfile}
+1 -1
View File
@@ -139,7 +139,7 @@ function Settings() {
className={styles.link} className={styles.link}
to="/settings/ui" to="/settings/ui"
> >
{translate('Ui')} {translate('UI')}
</Link> </Link>
<div className={styles.summary}> <div className={styles.summary}>
@@ -4,8 +4,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import monitorOptions from 'Utilities/Author/monitorOptions';
// //
// Variables // Variables
@@ -53,10 +51,6 @@ export default {
port: 8080, port: 8080,
useSsl: false, useSsl: false,
outputProfile: 'default', outputProfile: 'default',
defaultQualityProfileId: 0,
defaultMetadataProfileId: 0,
defaultMonitorOption: monitorOptions[0].key,
defaultNewItemMonitorOption: monitorNewItemsOptions[0].key,
defaultTags: [] defaultTags: []
}, },
isSaving: false, isSaving: false,
+1 -20
View File
@@ -4,7 +4,6 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) { function getDimensions(width, height) {
@@ -42,12 +41,7 @@ export const defaultState = {
isReconnecting: false, isReconnecting: false,
isDisconnected: false, isDisconnected: false,
isRestarting: false, isRestarting: false,
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen, isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen
translations: {
isFetching: true,
isPopulated: false,
error: null
}
}; };
// //
@@ -59,7 +53,6 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions';
export const SET_VERSION = 'app/setVersion'; export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue'; export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
export const PING_SERVER = 'app/pingServer'; 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 showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE); export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER); export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
// //
// Helpers // Helpers
@@ -135,17 +127,6 @@ function pingServerAfterTimeout(getState, dispatch) {
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) { [PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, 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'
}
}));
} }
}); });
+5 -5
View File
@@ -24,12 +24,12 @@ export const section = 'books';
export const filters = [ export const filters = [
{ {
key: 'all', key: 'all',
label: () => translate('All'), label: translate('All'),
filters: [] filters: []
}, },
{ {
key: 'monitored', key: 'monitored',
label: () => translate('Monitored'), label: translate('Monitored'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -40,7 +40,7 @@ export const filters = [
}, },
{ {
key: 'unmonitored', key: 'unmonitored',
label: () => translate('Unmonitored'), label: translate('Unmonitored'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -51,7 +51,7 @@ export const filters = [
}, },
{ {
key: 'missing', key: 'missing',
label: () => translate('Missing'), label: translate('Missing'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -67,7 +67,7 @@ export const filters = [
}, },
{ {
key: 'wanted', key: 'wanted',
label: () => translate('Wanted'), label: translate('Wanted'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
+16 -16
View File
@@ -60,32 +60,32 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'status', name: 'status',
columnLabel: () => translate('Status'), columnLabel: translate('Status'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'authorMetadata.sortName', name: 'authorMetadata.sortName',
label: () => translate('Author'), label: translate('Author'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'books.title', name: 'books.title',
label: () => translate('BookTitle'), label: translate('BookTitle'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'books.releaseDate', name: 'books.releaseDate',
label: () => translate('ReleaseDate'), label: translate('ReleaseDate'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'quality', name: 'quality',
label: () => translate('Quality'), label: translate('Quality'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
@@ -97,64 +97,64 @@ export const defaultState = {
}, },
{ {
name: 'customFormatScore', name: 'customFormatScore',
columnLabel: () => translate('CustomFormatScore'), columnLabel: translate('CustomFormatScore'),
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: () => translate('CustomFormatScore') title: translate('CustomFormatScore')
}), }),
isVisible: false isVisible: false
}, },
{ {
name: 'protocol', name: 'protocol',
label: () => translate('Protocol'), label: translate('Protocol'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'indexer', name: 'indexer',
label: () => translate('Indexer'), label: translate('Indexer'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'downloadClient', name: 'downloadClient',
label: () => translate('DownloadClient'), label: translate('DownloadClient'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'title', name: 'title',
label: () => translate('ReleaseTitle'), label: translate('ReleaseTitle'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'size', name: 'size',
label: () => translate('Size'), label: translate('Size'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'outputPath', name: 'outputPath',
label: () => translate('OutputPath'), label: translate('OutputPath'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'estimatedCompletionTime', name: 'estimatedCompletionTime',
label: () => translate('TimeLeft'), label: translate('TimeLeft'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'progress', name: 'progress',
label: () => translate('Progress'), label: translate('Progress'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'actions', name: 'actions',
columnLabel: () => translate('Actions'), columnLabel: translate('Actions'),
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
} }
+1 -1
View File
@@ -199,7 +199,7 @@ export const defaultState = {
}, },
{ {
name: 'customFormatScore', name: 'customFormatScore',
label: () => translate('CustomFormatScore'), label: translate('CustomFormatScore'),
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER
}, },
{ {
+5 -5
View File
@@ -82,34 +82,34 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'level', name: 'level',
columnLabel: () => translate('Level'), columnLabel: translate('Level'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'time', name: 'time',
label: () => translate('Time'), label: translate('Time'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'logger', name: 'logger',
label: () => translate('Component'), label: translate('Component'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'message', name: 'message',
label: () => translate('Message'), label: translate('Message'),
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'actions', name: 'actions',
columnLabel: () => translate('Actions'), columnLabel: translate('Actions'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
@@ -36,17 +36,10 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const column = initialColumns.find((i) => i.name === persistedColumn.name); const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) { if (column) {
const newColumn = {}; columns.push({
...column,
// We can't use a spread operator or Object.assign to clone the column isVisible: persistedColumn.isVisible
// 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);
} }
}); });
+1 -1
View File
@@ -138,7 +138,7 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteBackup')} title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', { name })} message={translate('DeleteBackupMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress} onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
+3 -3
View File
@@ -21,17 +21,17 @@ const columns = [
}, },
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: 'Name',
isVisible: true isVisible: true
}, },
{ {
name: 'size', name: 'size',
label: () => translate('Size'), label: 'Size',
isVisible: true isVisible: true
}, },
{ {
name: 'time', name: 'time',
label: () => translate('Time'), label: 'Time',
isVisible: true isVisible: true
}, },
{ {
@@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component {
<ModalBody> <ModalBody>
{ {
!!id && translate('WouldYouLikeToRestoreBackup', { name }) !!id && `Would you like to restore the backup '${name}'?`
} }
{ {
+2 -2
View File
@@ -19,12 +19,12 @@ import LogFilesTableRow from './LogFilesTableRow';
const columns = [ const columns = [
{ {
name: 'filename', name: 'filename',
label: () => translate('Filename'), label: 'Filename',
isVisible: true isVisible: true
}, },
{ {
name: 'lastWriteTime', name: 'lastWriteTime',
label: () => translate('LastWriteTime'), label: 'Last Write Time',
isVisible: true isVisible: true
}, },
{ {
@@ -15,17 +15,17 @@ import styles from './DiskSpace.css';
const columns = [ const columns = [
{ {
name: 'path', name: 'path',
label: () => translate('Location'), label: 'Location',
isVisible: true isVisible: true
}, },
{ {
name: 'freeSpace', name: 'freeSpace',
label: () => translate('FreeSpace'), label: 'Free Space',
isVisible: true isVisible: true
}, },
{ {
name: 'totalSpace', name: 'totalSpace',
label: () => translate('TotalSpace'), label: 'Total Space',
isVisible: true isVisible: true
}, },
{ {
+2 -10
View File
@@ -39,14 +39,6 @@ function getInternalLink(source) {
to="/settings/downloadclients" to="/settings/downloadclients"
/> />
); );
case 'NotificationStatusCheck':
return (
<IconButton
name={icons.SETTINGS}
title={translate('Settings')}
to="/settings/connect"
/>
);
case 'RootFolderCheck': case 'RootFolderCheck':
return ( return (
<IconButton <IconButton
@@ -103,12 +95,12 @@ const columns = [
}, },
{ {
name: 'message', name: 'message',
label: () => translate('Message'), label: 'Message',
isVisible: true isVisible: true
}, },
{ {
name: 'actions', name: 'actions',
label: () => translate('Actions'), label: 'Actions',
isVisible: true isVisible: true
} }
]; ];
@@ -15,27 +15,27 @@ const columns = [
}, },
{ {
name: 'commandName', name: 'commandName',
label: () => translate('Name'), label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'queued', name: 'queued',
label: () => translate('Queued'), label: translate('Queued'),
isVisible: true isVisible: true
}, },
{ {
name: 'started', name: 'started',
label: () => translate('Started'), label: translate('Started'),
isVisible: true isVisible: true
}, },
{ {
name: 'ended', name: 'ended',
label: () => translate('Ended'), label: translate('Ended'),
isVisible: true isVisible: true
}, },
{ {
name: 'duration', name: 'duration',
label: () => translate('Duration'), label: translate('Duration'),
isVisible: true isVisible: true
}, },
{ {
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [ const columns = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: 'Name',
isVisible: true isVisible: true
}, },
{ {
name: 'interval', name: 'interval',
label: () => translate('Interval'), label: 'Interval',
isVisible: true isVisible: true
}, },
{ {
name: 'lastExecution', name: 'lastExecution',
label: () => translate('LastExecution'), label: 'Last Execution',
isVisible: true isVisible: true
}, },
{ {
name: 'lastDuration', name: 'lastDuration',
label: () => translate('LastDuration'), label: 'Last Duration',
isVisible: true isVisible: true
}, },
{ {
name: 'nextExecution', name: 'nextExecution',
label: () => translate('NextExecution'), label: 'Next Execution',
isVisible: true isVisible: true
}, },
{ {
@@ -0,0 +1,36 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization'
}).request;
}
let translations = {};
export function fetchTranslations() {
return new Promise(async(resolve) => {
try {
const data = await getTranslations();
translations = data.Strings;
resolve(true);
} catch (error) {
resolve(false);
}
});
}
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;
}
@@ -1,44 +0,0 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization',
}).request;
}
let translations: Record<string, string> = {};
export async function fetchTranslations(): Promise<boolean> {
return new Promise(async (resolve) => {
try {
const data = await getTranslations();
translations = data.Strings;
resolve(true);
} catch (error) {
resolve(false);
}
});
}
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
) {
const translation = translations[key] || key;
if (tokens) {
// 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)
);
}
return translation;
}
-15
View File
@@ -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(
<App store={store} history={history} />,
document.getElementById('root')
);
}
+4 -9
View File
@@ -48,15 +48,7 @@
/> />
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css"> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
<!-- webpack bundles head -->
<script>
window.Readarr = {
urlBase: '__URL_BASE__'
};
</script>
<% for (key in htmlWebpackPlugin.files.js) { %><script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[key] %>" data-no-hash></script><% } %>
<% for (key in htmlWebpackPlugin.files.css) { %><link href="<%= htmlWebpackPlugin.files.css[key] %>" rel="stylesheet"></link><% } %>
<title>Readarr</title> <title>Readarr</title>
@@ -85,4 +77,7 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<div id="root" class="root"></div> <div id="root" class="root"></div>
</body> </body>
<script src="/initialize.js" data-no-hash></script>
<!-- webpack bundles body -->
</html> </html>
+26
View File
@@ -0,0 +1,26 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import { fetchTranslations } from 'Utilities/String/translate';
import './preload';
import './polyfills';
import 'Styles/globals.css';
import './index.css';
const history = createBrowserHistory();
const hasTranslationsError = !await fetchTranslations();
const { default: createAppStore } = await import('Store/createAppStore');
const { default: App } = await import('./App/App');
const store = createAppStore(history);
render(
<App
store={store}
history={history}
hasTranslationsError={hasTranslationsError}
/>,
document.getElementById('root')
);
-19
View File
@@ -1,19 +0,0 @@
import './polyfills';
import 'Styles/globals.css';
import './index.css';
const initializeUrl = `${
window.Readarr.urlBase
}/initialize.json?t=${Date.now()}`;
const response = await fetch(initializeUrl);
window.Readarr = await response.json();
/* eslint-disable no-undef, @typescript-eslint/ban-ts-comment */
// @ts-ignore 2304
__webpack_public_path__ = `${window.Readarr.urlBase}/`;
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
const { bootstrap } = await import('./bootstrap');
await bootstrap();
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "es6",
"allowJs": true, "allowJs": true,
"checkJs": false, "checkJs": false,
"baseUrl": "src", "baseUrl": "src",
"jsx": "react", "jsx": "react",
"module": "esnext", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
+20 -19
View File
@@ -20,7 +20,7 @@
"author": "Team Readarr", "author": "Team Readarr",
"license": "GPL-3.0", "license": "GPL-3.0",
"readmeFilename": "readme.md", "readmeFilename": "readme.md",
"main": "index.ts", "main": "index.js",
"browserslist": [ "browserslist": [
"defaults" "defaults"
], ],
@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.21", "@microsoft/signalr": "6.0.16",
"@sentry/browser": "7.51.2", "@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2", "@sentry/integrations": "7.51.2",
"@types/node": "18.16.16", "@types/node": "18.16.16",
@@ -83,27 +83,28 @@
"redux-localstorage": "0.4.1", "redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0", "redux-thunk": "2.3.0",
"reselect": "4.1.8", "reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5" "typescript": "4.9.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.11", "@babel/core": "7.22.9",
"@babel/eslint-parser": "7.22.11", "@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-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/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5", "@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.11", "@babel/preset-typescript": "7.22.5",
"@types/lodash": "4.14.197", "@typescript-eslint/eslint-plugin": "6.0.0",
"@types/redux-actions": "2.6.2", "@typescript-eslint/parser": "6.0.0",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.32.1", "core-js": "3.31.1",
"css-loader": "6.8.1", "css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1", "css-modules-typescript-loader": "4.0.1",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "8.8.0",
@@ -117,9 +118,9 @@
"file-loader": "6.2.0", "file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0", "filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.3", "html-webpack-plugin": "5.5.1",
"loader-utils": "^3.2.1", "loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.6", "mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.23", "postcss": "8.4.23",
"postcss-color-function": "4.1.0", "postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0", "postcss-loader": "7.3.0",
@@ -132,14 +133,14 @@
"rimraf": "4.4.1", "rimraf": "4.4.1",
"run-sequence": "2.2.1", "run-sequence": "2.2.1",
"streamqueue": "1.1.2", "streamqueue": "1.1.2",
"style-loader": "3.3.3", "style-loader": "3.3.2",
"stylelint": "15.10.3", "stylelint": "15.10.1",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"ts-loader": "9.4.4", "ts-loader": "9.4.3",
"typescript-plugin-css-modules": "5.0.1", "typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"webpack": "5.88.2", "webpack": "5.88.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2", "webpack-livereload-plugin": "3.0.2",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
@@ -44,16 +44,16 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}" Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
[Dirs] [Dirs]
Name: "{app}"; Permissions: users-modify Name: "{app}"; Permissions: users-modify
[Files] [Files]
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Readarr\Readarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion Source: "..\_artifacts\{#Runtime}\{#Framework}\Readarr\Readarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Readarr\*"; Excludes: "Readarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "..\_artifacts\{#Runtime}\{#Framework}\Readarr\*"; Excludes: "Readarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons] [Icons]
@@ -72,13 +72,12 @@ Filename: "{app}\bin\Readarr.exe"; Description: "Open Readarr Web UI"; Flags: po
Filename: "{app}\bin\Readarr.exe"; Description: "Start Readarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none; Filename: "{app}\bin\Readarr.exe"; Description: "Start Readarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
[UninstallRun] [UninstallRun]
Filename: "{app}\bin\readarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist Filename: "{app}\bin\Readarr.Console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
[Code] [Code]
function PrepareToInstall(var NeedsRestart: Boolean): String; function PrepareToInstall(var NeedsRestart: Boolean): String;
var var
ResultCode: Integer; ResultCode: Integer;
begin begin
Exec('net', 'stop readarr', '', 0, ewWaitUntilTerminated, ResultCode) Exec(ExpandConstant('{commonappdata}\Readarr\bin\Readarr.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('sc', 'delete readarr', '', 0, ewWaitUntilTerminated, ResultCode)
end; end;
+7 -7
View File
@@ -4,7 +4,7 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" /> <PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" /> <PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.123" /> <PackageVersion Include="Dapper" Version="2.0.123" />
<PackageVersion Include="DryIoc.dll" Version="5.4.1" /> <PackageVersion Include="DryIoc.dll" Version="5.4.0" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" /> <PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" /> <PackageVersion Include="FluentAssertions" Version="5.10.3" />
@@ -16,7 +16,7 @@
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" /> <PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" /> <PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" /> <PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.21" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.16" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
@@ -32,7 +32,7 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.2.3" />
<PackageVersion Include="NLog" Version="5.1.4" /> <PackageVersion Include="NLog" Version="5.1.4" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="7.0.4" /> <PackageVersion Include="Npgsql" Version="6.0.9" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
@@ -43,7 +43,7 @@
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" /> <PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" /> <PackageVersion Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.5.1" />
@@ -57,10 +57,10 @@
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" /> <PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" /> <PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" /> <PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.8" /> <PackageVersion Include="System.Text.Json" Version="6.0.7" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" /> <PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" /> <PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -40,10 +40,6 @@ namespace NzbDrone.Automation.Test.PageModel
var element = d.FindElement(By.ClassName("followingBalls")); var element = d.FindElement(By.ClassName("followingBalls"));
return !element.Displayed; return !element.Displayed;
} }
catch (StaleElementReferenceException)
{
return true;
}
catch (NoSuchElementException) catch (NoSuchElementException)
{ {
return true; return true;
@@ -838,7 +838,7 @@ namespace NzbDrone.Common.Test.DiskTests
// Note: never returns anything. // Note: never returns anything.
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileInfos(It.IsAny<string>(), It.IsAny<bool>())) .Setup(v => v.GetFileInfos(It.IsAny<string>(), It.IsAny<SearchOption>()))
.Returns(new List<IFileInfo>()); .Returns(new List<IFileInfo>());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
@@ -878,8 +878,8 @@ namespace NzbDrone.Common.Test.DiskTests
.Returns<string>(v => fileSystem.DirectoryInfo.FromDirectoryName(v).GetDirectories().ToList()); .Returns<string>(v => fileSystem.DirectoryInfo.FromDirectoryName(v).GetDirectories().ToList());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileInfos(It.IsAny<string>(), It.IsAny<bool>())) .Setup(v => v.GetFileInfos(It.IsAny<string>(), It.IsAny<SearchOption>()))
.Returns((string v, bool recursive) => fileSystem.DirectoryInfo.FromDirectoryName(v).GetFiles("*", new EnumerationOptions { RecurseSubdirectories = recursive }).ToList()); .Returns((string v, SearchOption option) => fileSystem.DirectoryInfo.FromDirectoryName(v).GetFiles("*", option).ToList());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetFileSize(It.IsAny<string>())) .Setup(v => v.GetFileSize(It.IsAny<string>()))
@@ -6,7 +6,6 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NLog; using NLog;
@@ -115,21 +114,21 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_execute_simple_get() public void should_execute_simple_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.ExecuteAsync(request); var response = Subject.Execute(request);
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[Test] [Test]
public async Task should_execute_https_get() public void should_execute_https_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.ExecuteAsync(request); var response = Subject.Execute(request);
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
@@ -141,47 +140,47 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType); Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com"); var request = new HttpRequest($"https://expired.badssl.com");
Assert.ThrowsAsync<HttpRequestException>(async () => await Subject.ExecuteAsync(request)); Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public async Task bad_ssl_should_pass_if_remote_validation_disabled() public void bad_ssl_should_pass_if_remote_validation_disabled()
{ {
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled); Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com"); var request = new HttpRequest($"https://expired.badssl.com");
await Subject.ExecuteAsync(request); Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
} }
[Test] [Test]
public async Task should_execute_typed_get() public void should_execute_typed_get()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get?test=1"); var request = new HttpRequest($"https://{_httpBinHost}/get?test=1");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Url.EndsWith("/get?test=1"); response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1"); response.Resource.Args.Should().Contain("test", "1");
} }
[Test] [Test]
public async Task should_execute_simple_post() public void should_execute_simple_post()
{ {
var message = "{ my: 1 }"; var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post"); var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message); request.SetContent(message);
var response = await Subject.PostAsync<HttpBinResource>(request); var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[Test] [Test]
public async Task should_execute_post_with_content_type() public void should_execute_post_with_content_type()
{ {
var message = "{ my: 1 }"; var message = "{ my: 1 }";
@@ -189,16 +188,17 @@ namespace NzbDrone.Common.Test.Http
request.SetContent(message); request.SetContent(message);
request.Headers.ContentType = "application/json"; request.Headers.ContentType = "application/json";
var response = await Subject.PostAsync<HttpBinResource>(request); var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[Test] [Test]
public async Task should_execute_get_using_gzip() public void should_execute_get_using_gzip()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/gzip"); var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = await Subject.GetAsync<HttpBinResource>(request);
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip"); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
@@ -208,10 +208,11 @@ namespace NzbDrone.Common.Test.Http
[Test] [Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")] [Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public async Task should_execute_get_using_brotli() public void should_execute_get_using_brotli()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/brotli"); var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = await Subject.GetAsync<HttpBinResource>(request);
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br"); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
@@ -229,7 +230,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}");
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request)); var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
((int)exception.Response.StatusCode).Should().Be(statusCode); ((int)exception.Response.StatusCode).Should().Be(statusCode);
@@ -242,7 +243,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound }; request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request)); var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
@@ -252,7 +253,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request)); var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
@@ -263,28 +264,28 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}"); var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false; request.LogHttpError = false;
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request)); var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0); ExceptionVerification.ExpectedWarns(0);
} }
[Test] [Test]
public async Task should_not_follow_redirects_when_not_in_production() public void should_not_follow_redirects_when_not_in_production()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
await Subject.GetAsync(request); Subject.Get(request);
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public async Task should_follow_redirects() public void should_follow_redirects()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
var response = await Subject.GetAsync(request); var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -292,12 +293,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_not_follow_redirects() public void should_not_follow_redirects()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false; request.AllowAutoRedirect = false;
var response = await Subject.GetAsync(request); var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Found); response.StatusCode.Should().Be(HttpStatusCode.Found);
@@ -305,14 +306,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_follow_redirects_to_https() public void should_follow_redirects_to_https()
{ {
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to") var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/") .AddQueryParam("url", $"https://readarr.com/")
.Build(); .Build();
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
var response = await Subject.GetAsync(request); var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Should().Contain("Readarr"); response.Content.Should().Contain("Readarr");
@@ -326,17 +327,17 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/redirect/6"); var request = new HttpRequest($"https://{_httpBinHost}/redirect/6");
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
Assert.ThrowsAsync<WebException>(async () => await Subject.GetAsync(request)); Assert.Throws<WebException>(() => Subject.Get(request));
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
} }
[Test] [Test]
public async Task should_send_user_agent() public void should_send_user_agent()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("User-Agent"); response.Resource.Headers.Should().ContainKey("User-Agent");
@@ -346,24 +347,24 @@ namespace NzbDrone.Common.Test.Http
} }
[TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")] [TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")]
public async Task should_send_headers(string header, string value) public void should_send_headers(string header, string value)
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Headers.Add(header, value); request.Headers.Add(header, value);
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers[header].ToString().Should().Be(value); response.Resource.Headers[header].ToString().Should().Be(value);
} }
[Test] [Test]
public async Task should_download_file() public void should_download_file()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
var url = "https://readarr.com/img/slider/artistdetails.png"; var url = "https://readarr.com/img/slider/artistdetails.png";
await Subject.DownloadFileAsync(url, file); Subject.DownloadFile(url, file);
var fileInfo = new FileInfo(file); var fileInfo = new FileInfo(file);
fileInfo.Exists.Should().BeTrue(); fileInfo.Exists.Should().BeTrue();
@@ -371,7 +372,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_download_file_with_redirect() public void should_download_file_with_redirect()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
@@ -379,7 +380,7 @@ namespace NzbDrone.Common.Test.Http
.AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png") .AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png")
.Build(); .Build();
await Subject.DownloadFileAsync(request.Url.FullUri, file); Subject.DownloadFile(request.Url.FullUri, file);
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(0);
@@ -393,7 +394,7 @@ namespace NzbDrone.Common.Test.Http
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
Assert.ThrowsAsync<HttpException>(async () => await Subject.DownloadFileAsync("https://download.sonarr.tv/wrongpath", file)); Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse(); File.Exists(file).Should().BeFalse();
@@ -401,7 +402,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_not_write_redirect_content_to_stream() public void should_not_write_redirect_content_to_stream()
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
@@ -411,7 +412,7 @@ namespace NzbDrone.Common.Test.Http
request.AllowAutoRedirect = false; request.AllowAutoRedirect = false;
request.ResponseStream = fileStream; request.ResponseStream = fileStream;
var response = await Subject.GetAsync(request); var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved); response.StatusCode.Should().Be(HttpStatusCode.Moved);
} }
@@ -426,12 +427,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_send_cookie() public void should_send_cookie()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Cookies["my"] = "cookie"; request.Cookies["my"] = "cookie";
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -460,13 +461,13 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_preserve_cookie_during_session() public void should_preserve_cookie_during_session()
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost2}/get"); var request = new HttpRequest($"https://{_httpBinHost2}/get");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -476,30 +477,30 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_not_send_cookie_to_other_host() public void should_not_send_cookie_to_other_host()
{ {
GivenOldCookie(); GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie"); response.Resource.Headers.Should().NotContainKey("Cookie");
} }
[Test] [Test]
public async Task should_not_store_request_cookie() public void should_not_store_request_cookie()
{ {
var requestGet = new HttpRequest($"https://{_httpBinHost}/get"); var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie"); requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false; requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie = false; requestGet.StoreRequestCookie = false;
requestGet.StoreResponseCookie = false; requestGet.StoreResponseCookie = false;
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet); var responseGet = Subject.Get<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -507,18 +508,18 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_store_request_cookie() public void should_store_request_cookie()
{ {
var requestGet = new HttpRequest($"https://{_httpBinHost}/get"); var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie"); requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false; requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie.Should().BeTrue(); requestGet.StoreRequestCookie.Should().BeTrue();
requestGet.StoreResponseCookie = false; requestGet.StoreResponseCookie = false;
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet); var responseGet = Subject.Get<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -526,7 +527,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_delete_request_cookie() public void should_delete_request_cookie()
{ {
var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my"); var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my");
requestDelete.Cookies.Add("my", "cookie"); requestDelete.Cookies.Add("my", "cookie");
@@ -535,13 +536,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
// Delete and redirect since that's the only way to check the internal temporary cookie container // Delete and redirect since that's the only way to check the internal temporary cookie container
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestDelete); var responseCookies = Subject.Get<HttpCookieResource>(requestDelete);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
} }
[Test] [Test]
public async Task should_clear_request_cookie() public void should_clear_request_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies");
requestSet.Cookies.Add("my", "cookie"); requestSet.Cookies.Add("my", "cookie");
@@ -549,7 +550,7 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true; requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false; requestSet.StoreResponseCookie = false;
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet); var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies");
requestClear.Cookies.Add("my", null); requestClear.Cookies.Add("my", null);
@@ -557,24 +558,24 @@ namespace NzbDrone.Common.Test.Http
requestClear.StoreRequestCookie = true; requestClear.StoreRequestCookie = true;
requestClear.StoreResponseCookie = false; requestClear.StoreResponseCookie = false;
var responseClear = await Subject.GetAsync<HttpCookieResource>(requestClear); var responseClear = Subject.Get<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty(); responseClear.Resource.Cookies.Should().BeEmpty();
} }
[Test] [Test]
public async Task should_not_store_response_cookie() public void should_not_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse(); requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = await Subject.GetAsync(requestSet); var responseSet = Subject.Get(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -582,18 +583,18 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_store_response_cookie() public void should_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = await Subject.GetAsync(requestSet); var responseSet = Subject.Get(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -601,13 +602,13 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_temp_store_response_cookie() public void should_temp_store_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = true; requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse(); requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet); var responseSet = Subject.Get<HttpCookieResource>(requestSet);
// Set and redirect since that's the only way to check the internal temporary cookie container // Set and redirect since that's the only way to check the internal temporary cookie container
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -616,7 +617,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_overwrite_response_cookie() public void should_overwrite_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie"); requestSet.Cookies.Add("my", "oldcookie");
@@ -624,11 +625,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = false; requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = await Subject.GetAsync(requestSet); var responseSet = Subject.Get(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -636,7 +637,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_overwrite_temp_response_cookie() public void should_overwrite_temp_response_cookie()
{ {
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie"); var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie"); requestSet.Cookies.Add("my", "oldcookie");
@@ -644,13 +645,13 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true; requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false; requestSet.StoreResponseCookie = false;
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet); var responseSet = Subject.Get<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
@@ -658,14 +659,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_not_delete_response_cookie() public void should_not_delete_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -674,13 +675,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
var responseDelete = await Subject.GetAsync(requestDelete); var responseDelete = Subject.Get(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false; requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -688,14 +689,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_delete_response_cookie() public void should_delete_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -704,13 +705,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = true; requestDelete.StoreResponseCookie = true;
var responseDelete = await Subject.GetAsync(requestDelete); var responseDelete = Subject.Get(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false; requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty(); responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -718,14 +719,14 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_delete_temp_response_cookie() public void should_delete_temp_response_cookie()
{ {
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies"); var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie"); requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false; requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true; requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false; requestCookies.StoreResponseCookie = false;
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies); var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie"); responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -733,7 +734,7 @@ namespace NzbDrone.Common.Test.Http
requestDelete.AllowAutoRedirect = true; requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false; requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false; requestDelete.StoreResponseCookie = false;
var responseDelete = await Subject.GetAsync<HttpCookieResource>(requestDelete); var responseDelete = Subject.Get<HttpCookieResource>(requestDelete);
responseDelete.Resource.Cookies.Should().BeEmpty(); responseDelete.Resource.Cookies.Should().BeEmpty();
@@ -751,13 +752,13 @@ namespace NzbDrone.Common.Test.Http
{ {
var request = new HttpRequest($"https://{_httpBinHost}/status/429"); var request = new HttpRequest($"https://{_httpBinHost}/status/429");
Assert.ThrowsAsync<TooManyRequestsException>(async () => await Subject.GetAsync(request)); Assert.Throws<TooManyRequestsException>(() => Subject.Get(request));
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test] [Test]
public async Task should_call_interceptor() public void should_call_interceptor()
{ {
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object }); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object });
@@ -771,7 +772,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
await Subject.GetAsync(request); Subject.Get(request);
Mocker.GetMock<IHttpRequestInterceptor>() Mocker.GetMock<IHttpRequestInterceptor>()
.Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once()); .Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once());
@@ -782,7 +783,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("en-US")] [TestCase("en-US")]
[TestCase("es-ES")] [TestCase("es-ES")]
public async Task should_parse_malformed_cloudflare_cookie(string culture) public void should_parse_malformed_cloudflare_cookie(string culture)
{ {
var origCulture = Thread.CurrentThread.CurrentCulture; var origCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
@@ -798,11 +799,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = await Subject.GetAsync(requestSet); var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie"); response.Resource.Headers.Should().ContainKey("Cookie");
@@ -820,7 +821,7 @@ namespace NzbDrone.Common.Test.Http
} }
[TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")] [TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")]
public async Task should_reject_malformed_domain_cookie(string malformedCookie) public void should_reject_malformed_domain_cookie(string malformedCookie)
{ {
try try
{ {
@@ -830,11 +831,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
var responseSet = await Subject.GetAsync(requestSet); var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get"); var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = await Subject.GetAsync<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie"); response.Resource.Headers.Should().NotContainKey("Cookie");
@@ -846,12 +847,12 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
public async Task should_correctly_use_basic_auth() public void should_correctly_use_basic_auth_with_basic_network_credential()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password"); var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password");
request.Credentials = new BasicNetworkCredential("username", "password"); request.Credentials = new BasicNetworkCredential("username", "password");
var response = await Subject.ExecuteAsync(request); var response = Subject.Execute(request);
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
} }
+2 -3
View File
@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.GZip;
@@ -12,7 +11,7 @@ namespace NzbDrone.Common
public interface IArchiveService public interface IArchiveService
{ {
void Extract(string compressedFile, string destination); void Extract(string compressedFile, string destination);
void CreateZip(string path, IEnumerable<string> files); void CreateZip(string path, params string[] files);
} }
public class ArchiveService : IArchiveService public class ArchiveService : IArchiveService
@@ -40,7 +39,7 @@ namespace NzbDrone.Common
_logger.Debug("Extraction complete."); _logger.Debug("Extraction complete.");
} }
public void CreateZip(string path, IEnumerable<string> files) public void CreateZip(string path, params string[] files)
{ {
using (var zipFile = ZipFile.Create(path)) using (var zipFile = ZipFile.Create(path))
{ {

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