Compare commits

..

1 Commits

Author SHA1 Message Date
ta264
dddcb89c5b Allow build script to produce packages on non-windows OSes
(cherry picked from commit 45ab2b2409fd11c556c1374aec8d27aec898c4fc)
2022-04-29 11:01:27 +00:00
205 changed files with 1609 additions and 6267 deletions

View File

@@ -15,7 +15,7 @@ variables:
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.302' dotnetVersion: '6.0.201'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
@@ -66,13 +66,16 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Extra Platform Support displayName: Enable FreeBSD Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
@@ -84,27 +87,29 @@ stages:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item - powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output displayName: Clean up intermediate output
- publish: $(outputFolder) - task: PublishPipelineArtifact@1
artifact: '$(osName)Backend' inputs:
path: $(outputFolder)
artifact: '$(osName)Backend'
artifactType: 'pipeline'
parallel: true
parallelCount: 100
displayName: Publish Backend displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests artifact: WindowsCoreTests
displayName: Publish win-x64 Test Package displayName: Publish Windows Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests artifact: LinuxCoreTests
displayName: Publish linux-x64 Test Package displayName: Publish Linux 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' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests artifact: LinuxMuslCoreTests
displayName: Publish linux-musl-x64 Test Package displayName: Publish Linux Musl Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests artifact: FreebsdCoreTests
displayName: Publish freebsd-x64 Test Package displayName: Publish FreeBSD Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests artifact: MacCoreTests
displayName: Publish osx-x64 Test Package displayName: Publish MacOS Test Package
- stage: Build_Backend_Other - stage: Build_Backend_Other
displayName: Build Backend (Other OS) displayName: Build Backend (Other OS)
@@ -136,29 +141,25 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Extra Platform Support displayName: Enable FreeBSD Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder) path: $(nugetCacheFolder)
displayName: Cache NuGet packages displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-extra-platforms - bash: ./build.sh --backend --enable-bsd
displayName: Build Readarr Backend displayName: Build Readarr Backend
env: env:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
displayName: Frontend displayName: Frontend
@@ -261,35 +262,35 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-extra-platforms - bash: ./build.sh --packages --enable-bsd
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Readarr" -exec chmod a+x {} \; find . -name "Readarr" -exec chmod a+x {} \;
find . -name "Readarr.Update" -exec chmod a+x {} \; find . -name "Readarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x64 zip displayName: Create Windows Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x86 zip displayName: Create Windows x86 Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 app displayName: Create MacOS x64 Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 tar displayName: Create MacOS x64 Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -297,14 +298,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-arm64 app displayName: Create MacOS arm64 Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-arm64 tar displayName: Create MacOS arm64 Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -312,7 +313,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-x64 tar displayName: Create Linux Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -320,7 +321,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar displayName: Create Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -328,15 +329,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-x86 tar displayName: Create ARM32 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -344,7 +337,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm tar displayName: Create ARM32 Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -352,7 +345,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-arm64 tar displayName: Create Linux arm64 Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -360,7 +353,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar displayName: Create ARM64 Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -368,7 +361,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create freebsd-x64 tar displayName: Create FreeBSD Core Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -420,22 +413,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'freebsd-x64' testName: 'FreebsdCore'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -454,7 +447,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -482,12 +475,8 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -498,15 +487,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -529,57 +512,6 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
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: 'linux-x64-Tests'
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=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
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 Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
@@ -591,17 +523,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
@@ -618,7 +550,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -648,66 +580,6 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
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=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
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 Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
workspace: workspace:
@@ -723,7 +595,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'freebsd-x64-tests' artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -757,15 +629,11 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'linux-musl-x64' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Readarr.*.linux-musl-core-x64.tar.gz' pattern: 'Readarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Readarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -775,15 +643,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -829,17 +691,14 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
@@ -856,7 +715,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(artifactName)-tests' artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact

View File

@@ -27,22 +27,15 @@ UpdateVersionNumber()
fi fi
} }
EnableExtraPlatformsInSDK() EnableBsdSupport()
{ {
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') #todo enable sdk with
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" #SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then # BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -205,7 +198,10 @@ PackageWindows()
local folder=$artifactsFolder/$runtime/$framework/Readarr local folder=$artifactsFolder/$runtime/$framework/Readarr
PackageFiles "$folder" "$framework" "$runtime" PackageFiles "$folder" "$framework" "$runtime"
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
if [ "$os" = "windows" ]; then
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
fi
echo "Removing Readarr.Mono" echo "Removing Readarr.Mono"
rm -f $folder/Readarr.Mono.* rm -f $folder/Readarr.Mono.*
@@ -299,8 +295,7 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_EXTRA_PLATFORMS=NO ENABLE_BSD=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -312,12 +307,8 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd|--enable-extra-platforms) --enable-bsd)
ENABLE_EXTRA_PLATFORMS=YES ENABLE_BSD=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -361,17 +352,12 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
EnableExtraPlatforms EnableBsdSupport
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -381,10 +367,9 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -423,10 +408,9 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"

View File

@@ -8,17 +8,17 @@ function AuthorMonitorNewItemsOptionsPopoverContent() {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('AllBooks')} title={translate('AllBooks')}
data={translate('DataNewAllBooks')} data="Monitor all new books"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('NewBooks')} title={translate('NewBooks')}
data={translate('DataNewBooks')} data="Monitor new books released after the newest existing book"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data={translate('DataNewNone')} data="Don't monitor any new books"
/> />
</DescriptionList> </DescriptionList>
); );

View File

@@ -8,42 +8,42 @@ function AuthorMonitoringOptionsPopoverContent() {
return ( return (
<> <>
<Alert> <Alert>
{translate('MonitoringOptionsHelpText')} This is a one time adjustment to set which books are monitored
</Alert> </Alert>
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('AllBooks')} title={translate('AllBooks')}
data={translate('DataAllBooks')} data="Monitor all books"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('FutureBooks')} title={translate('FutureBooks')}
data={translate('DataFutureBooks')} data="Monitor books that have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('MissingBooks')} title={translate('MissingBooks')}
data={translate('DataMissingBooks')} data="Monitor books that do not have files or have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('ExistingBooks')} title={translate('ExistingBooks')}
data={translate('DataExistingBooks')} data="Monitor books that have files or have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('FirstBook')} title={translate('FirstBook')}
data={translate('DataFirstBook')} data="Monitor the first book. All other books will be ignored"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('LatestBook')} title={translate('LatestBook')}
data={translate('DataLatestBook')} data="Monitor the latest book and future books"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data={translate('DataNone')} data="No books will be monitored"
/> />
</DescriptionList> </DescriptionList>
</> </>

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title="Readarr">
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<PageConnector> <PageConnector>

View File

@@ -267,7 +267,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress} onPress={onOrganizeAuthorPress}
> >
{translate('RenameFiles')} Rename Files
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -277,7 +277,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress} onPress={onRetagAuthorPress}
> >
{translate('WriteMetadataTags')} Write Metadata Tags
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -286,7 +286,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress} onPress={this.onTagsPress}
> >
{translate('SetReadarrTags')} Set Readarr Tags
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -296,7 +296,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isDeleting} isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress} onPress={this.onDeleteSelectedPress}
> >
{translate('Delete')} Delete
</SpinnerButton> </SpinnerButton>
</div> </div>

View File

@@ -8,7 +8,6 @@ import * as commandNames from 'Commands/commandNames';
import { toggleBooksMonitored } from 'Store/Actions/bookActions'; import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions'; import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector'; import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -44,12 +43,11 @@ function createMapStateToProps() {
(state, { titleSlug }) => titleSlug, (state, { titleSlug }) => titleSlug,
selectBookFiles, selectBookFiles,
(state) => state.books, (state) => state.books,
(state) => state.editions,
createAllAuthorSelector(), createAllAuthorSelector(),
createCommandsSelector(), createCommandsSelector(),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => { (titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
const book = books.items.find((b) => b.titleSlug === titleSlug); const book = books.items.find((b) => b.titleSlug === titleSlug);
const author = authors.find((a) => a.id === book.authorId); const author = authors.find((a) => a.id === book.authorId);
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId); const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
@@ -81,8 +79,8 @@ function createMapStateToProps() {
isRefreshingCommand.body.bookId === book.id isRefreshingCommand.body.bookId === book.id
); );
const isFetching = isBookFilesFetching || editions.isFetching; const isFetching = isBookFilesFetching;
const isPopulated = isBookFilesPopulated && editions.isPopulated; const isPopulated = isBookFilesPopulated;
return { return {
...book, ...book,
@@ -106,8 +104,6 @@ const mapDispatchToProps = {
executeCommand, executeCommand,
fetchBookFiles, fetchBookFiles,
clearBookFiles, clearBookFiles,
fetchEditions,
clearEditions,
clearReleases, clearReleases,
cancelFetchReleases, cancelFetchReleases,
toggleBooksMonitored toggleBooksMonitored
@@ -125,8 +121,7 @@ class BookDetailsConnector extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id || if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate(); this.unpopulate();
this.populate(); this.populate();
@@ -145,14 +140,12 @@ class BookDetailsConnector extends Component {
const bookId = this.props.id; const bookId = this.props.id;
this.props.fetchBookFiles({ bookId }); this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
} }
unpopulate = () => { unpopulate = () => {
this.props.cancelFetchReleases(); this.props.cancelFetchReleases();
this.props.clearReleases(); this.props.clearReleases();
this.props.clearBookFiles(); this.props.clearBookFiles();
this.props.clearEditions();
} }
// //
@@ -202,8 +195,6 @@ BookDetailsConnector.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
fetchBookFiles: PropTypes.func.isRequired, fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired, clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired, clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired, cancelFetchReleases: PropTypes.func.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired, toggleBooksMonitored: PropTypes.func.isRequired,

View File

@@ -8,25 +8,15 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import BookDetailsHeader from './BookDetailsHeader'; import BookDetailsHeader from './BookDetailsHeader';
const selectOverview = createSelector(
(state) => state.editions,
(editions) => {
const monitored = editions.items.find((e) => e.monitored === true);
return monitored?.overview;
}
);
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
selectOverview,
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(book, overview, uiSettings, dimensions) => { (book, uiSettings, dimensions) => {
return { return {
...book, ...book,
overview,
shortDateFormat: uiSettings.shortDateFormat, shortDateFormat: uiSettings.shortDateFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };

View File

@@ -53,7 +53,7 @@ class EditBookModalContent extends Component {
editions editions
} = item; } = item;
const hasFile = statistics ? statistics.bookFileCount > 0 : false; const hasFile = statistics ? statistics.bookFileCount : 0;
const errorMessage = getErrorMessage(error, 'Unable to load editions'); const errorMessage = getErrorMessage(error, 'Unable to load editions');
return ( return (

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveBook, setBookValue } from 'Store/Actions/bookActions'; import { saveBook, setBookValue } from 'Store/Actions/bookActions';
import { saveEditions } from 'Store/Actions/editionActions'; import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
@@ -26,14 +26,17 @@ function createMapStateToProps() {
const { const {
isFetching, isFetching,
isPopulated, isPopulated,
error error,
items
} = editionState; } = editionState;
book.editions = items;
const bookSettings = _.pick(book, [ const bookSettings = _.pick(book, [
'monitored', 'monitored',
'anyEditionOk' 'anyEditionOk',
'editions'
]); ]);
bookSettings.editions = editionState.items;
const settings = selectSettings(bookSettings, pendingChanges, saveError); const settings = selectSettings(bookSettings, pendingChanges, saveError);
@@ -55,9 +58,10 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchEditions: fetchEditions,
dispatchClearEditions: clearEditions,
dispatchSetBookValue: setBookValue, dispatchSetBookValue: setBookValue,
dispatchSaveBook: saveBook, dispatchSaveBook: saveBook
dispatchSaveEditions: saveEditions
}; };
class EditBookModalContentConnector extends Component { class EditBookModalContentConnector extends Component {
@@ -65,12 +69,20 @@ class EditBookModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() {
this.props.dispatchFetchEditions({ bookId: this.props.bookId });
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose(); this.props.onModalClose();
} }
} }
componentWillUnmount() {
this.props.dispatchClearEditions();
}
// //
// Listeners // Listeners
@@ -82,9 +94,6 @@ class EditBookModalContentConnector extends Component {
this.props.dispatchSaveBook({ this.props.dispatchSaveBook({
id: this.props.bookId id: this.props.bookId
}); });
this.props.dispatchSaveEditions({
id: this.props.bookId
});
} }
// //
@@ -105,9 +114,10 @@ EditBookModalContentConnector.propTypes = {
bookId: PropTypes.number, bookId: PropTypes.number,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
dispatchFetchEditions: PropTypes.func.isRequired,
dispatchClearEditions: PropTypes.func.isRequired,
dispatchSetBookValue: PropTypes.func.isRequired, dispatchSetBookValue: PropTypes.func.isRequired,
dispatchSaveBook: PropTypes.func.isRequired, dispatchSaveBook: PropTypes.func.isRequired,
dispatchSaveEditions: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector'; import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
@@ -33,13 +32,11 @@ function selectShowSearchAction() {
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
createBookAuthorSelector(),
createBookQualityProfileSelector(), createBookQualityProfileSelector(),
selectShowSearchAction(), selectShowSearchAction(),
createExecutingCommandsSelector(), createExecutingCommandsSelector(),
( (
book, book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
executingCommands executingCommands
@@ -57,7 +54,7 @@ function createMapStateToProps() {
const isRefreshingBook = executingCommands.some((command) => { const isRefreshingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.REFRESH_AUTHOR && (command.name === commandNames.REFRESH_AUTHOR &&
command.body.authorId === book.authorId) || command.body.authorId === book.author.id) ||
(command.name === commandNames.REFRESH_BOOK && (command.name === commandNames.REFRESH_BOOK &&
command.body.bookId === book.id) command.body.bookId === book.id)
); );
@@ -66,7 +63,7 @@ function createMapStateToProps() {
const isSearchingBook = executingCommands.some((command) => { const isSearchingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.AUTHOR_SEARCH && (command.name === commandNames.AUTHOR_SEARCH &&
command.body.authorId === book.authorId) || command.body.authorId === book.author.id) ||
(command.name === commandNames.BOOK_SEARCH && (command.name === commandNames.BOOK_SEARCH &&
command.body.bookIds.includes(book.id)) command.body.bookIds.includes(book.id))
); );
@@ -74,7 +71,6 @@ function createMapStateToProps() {
return { return {
...book, ...book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
isRefreshingBook, isRefreshingBook,

View File

@@ -12,7 +12,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo'; import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@@ -43,26 +42,10 @@ class BookIndexOverview extends Component {
this.state = { this.state = {
isEditAuthorModalOpen: false, isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false, isDeleteAuthorModalOpen: false
overview: ''
}; };
} }
componentDidMount() {
const { id } = this.props;
// Note that this component is lazy loaded by the virtualised view.
// We want to avoid storing overviews for *all* books which is
// why it's not put into the redux store
const promise = createAjaxRequest({
url: `/book/${id}/overview`
}).request;
promise.done((data) => {
this.setState({ overview: data.overview });
});
}
// //
// Listeners // Listeners
@@ -101,6 +84,7 @@ class BookIndexOverview extends Component {
const { const {
id, id,
title, title,
overview,
monitored, monitored,
titleSlug, titleSlug,
nextAiring, nextAiring,
@@ -134,7 +118,6 @@ class BookIndexOverview extends Component {
} = statistics; } = statistics;
const { const {
overview,
isEditAuthorModalOpen, isEditAuthorModalOpen,
isDeleteAuthorModalOpen isDeleteAuthorModalOpen
} = this.state; } = this.state;
@@ -284,6 +267,7 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = { BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,

View File

@@ -6,7 +6,6 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './BookshelfFooter.css'; import styles from './BookshelfFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
@@ -115,7 +114,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('MonitorExistingBooks')} Monitor Existing Books
</div> </div>
<MonitorBooksSelectInput <MonitorBooksSelectInput
@@ -129,7 +128,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('MonitorNewBooks')} Monitor New Books
</div> </div>
<MonitorNewItemsSelectInput <MonitorNewItemsSelectInput
@@ -153,7 +152,7 @@ class BookshelfFooter extends Component {
isDisabled={!selectedCount || noChanges} isDisabled={!selectedCount || noChanges}
onPress={this.onUpdateSelectedPress} onPress={this.onUpdateSelectedPress}
> >
{translate('UpdateSelected')} Update Selected
</SpinnerButton> </SpinnerButton>
</div> </div>
</PageContentFooter> </PageContentFooter>

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
// import translate from 'Utilities/String/translate'; // import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput'; import AutoCompleteInput from './AutoCompleteInput';
import BookEditionSelectInputConnector from './BookEditionSelectInputConnector'; import BookEditionSelectInputConnector from './BookEditionSelectInputConnector';
@@ -217,7 +216,7 @@ function FormInputGroup(props) {
<Link <Link
to={helpLink} to={helpLink}
> >
{translate('MoreInfo')} More Info
</Link> </Link>
} }

View File

@@ -14,7 +14,7 @@ function PageContent(props) {
return ( return (
<ErrorBoundary errorComponent={PageContentError}> <ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - ${window.Readarr.instanceName}` : window.Readarr.instanceName}> <DocumentTitle title={title ? `${title} - Readarr` : 'Readarr'}>
<div className={className}> <div className={className}>
{children} {children}
</div> </div>

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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';
@@ -10,8 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
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 { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import SelectEditionRow from './SelectEditionRow';
import SelectEditionRowConnector from './SelectEditionRowConnector';
import styles from './SelectEditionModalContent.css'; import styles from './SelectEditionModalContent.css';
const columns = [ const columns = [
@@ -35,30 +33,15 @@ class SelectEditionModalContent extends Component {
render() { render() {
const { const {
books, books,
isPopulated,
isFetching,
error,
onEditionSelect, onEditionSelect,
onModalClose, onModalClose,
...otherProps ...otherProps
} = this.props; } = this.props;
if (!isPopulated && !error) {
return (<LoadingIndicator />);
}
if (!isFetching && error) {
return (
<div>
{translate('LoadingEditionsFailed')}
</div>
);
}
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('ManualImportSelectEdition')} Manual Import - Select Edition
</ModalHeader> </ModalHeader>
<ModalBody <ModalBody
@@ -77,7 +60,7 @@ class SelectEditionModalContent extends Component {
{ {
books.map((item) => { books.map((item) => {
return ( return (
<SelectEditionRowConnector <SelectEditionRow
key={item.book.id} key={item.book.id}
matchedEditionId={item.matchedEditionId} matchedEditionId={item.matchedEditionId}
columns={columns} columns={columns}
@@ -93,7 +76,7 @@ class SelectEditionModalContent extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
{translate('Cancel')} Cancel
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -103,9 +86,6 @@ class SelectEditionModalContent extends Component {
SelectEditionModalContent.propTypes = { SelectEditionModalContent.propTypes = {
books: PropTypes.arrayOf(PropTypes.object).isRequired, books: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool,
isPopulated: PropTypes.bool,
error: PropTypes.object,
onEditionSelect: PropTypes.func.isRequired, onEditionSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@@ -1,71 +1,27 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { import {
saveInteractiveImportItem, saveInteractiveImportItem,
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import SelectEditionModalContent from './SelectEditionModalContent'; import SelectEditionModalContent from './SelectEditionModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return {};
(state) => state.editions,
(editions) => {
const {
isFetching,
isPopulated,
error
} = editions;
return {
isFetching,
isPopulated,
error
};
}
);
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchEditions,
clearEditions,
updateInteractiveImportItem, updateInteractiveImportItem,
saveInteractiveImportItem saveInteractiveImportItem
}; };
class SelectEditionModalContentConnector extends Component { class SelectEditionModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
registerPagePopulator(this.populate);
this.populate();
}
componentWillUnmount() {
unregisterPagePopulator(this.populate);
this.unpopulate();
}
//
// Control
populate = () => {
const bookId = this.props.books.map((b) => b.book.id);
this.props.fetchEditions({ bookId });
}
unpopulate = () => {
this.props.clearEditions();
}
// //
// Listeners // Listeners
onEditionSelect = (bookId, foreignEditionId) => { onEditionSelect = (bookId, foreignEditionId) => {
console.log(`book: ${bookId} id: ${foreignEditionId} ${typeof foreignEditionId}`);
const ids = this.props.importIdsByBook[bookId]; const ids = this.props.importIdsByBook[bookId];
ids.forEach((id) => { ids.forEach((id) => {
@@ -99,8 +55,6 @@ class SelectEditionModalContentConnector extends Component {
SelectEditionModalContentConnector.propTypes = { SelectEditionModalContentConnector.propTypes = {
importIdsByBook: PropTypes.object.isRequired, importIdsByBook: PropTypes.object.isRequired,
books: PropTypes.arrayOf(PropTypes.object).isRequired, books: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired, updateInteractiveImportItem: PropTypes.func.isRequired,
saveInteractiveImportItem: PropTypes.func.isRequired, saveInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired

View File

@@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import SelectEditionRow from './SelectEditionRow';
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
(state) => state.editions,
(id, editionState) => {
const editions = editionState.items.filter((e) => e.bookId === id);
return { editions };
}
);
}
class SelectEditionRowConnector extends Component {
render() {
return (
<SelectEditionRow
{...this.props}
/>
);
}
}
SelectEditionRowConnector.PropTypes = {
editions: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps)(SelectEditionRowConnector);

View File

@@ -126,7 +126,7 @@ class AddAuthorOptionsForm extends Component {
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}> <FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
<FormLabel> <FormLabel>
{translate('MetadataProfile')} Metadata Profile
{ {
includeNoneMetadataProfile && includeNoneMetadataProfile &&

View File

@@ -20,7 +20,6 @@ function HostSettings(props) {
bindAddress, bindAddress,
port, port,
urlBase, urlBase,
instanceName,
enableSsl, enableSsl,
sslPort, sslPort,
sslCertPath, sslCertPath,
@@ -79,22 +78,6 @@ function HostSettings(props) {
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('InstanceName')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="instanceName"
helpText={translate('InstanceNameHelpText')}
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
onChange={onInputChange}
{...instanceName}
/>
</FormGroup>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}

View File

@@ -28,17 +28,17 @@ function ImportListMonitoringOptionsPopoverContent() {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data={translate('DataListMonitorNone')} data="Do not monitor authors or books"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('SpecificBook')} title={translate('SpecificBook')}
data={translate('DataListMonitorSpecificBook')} data="Monitor authors but only monitor books explicitly included in the list"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('AllAuthorBooks')} title={translate('AllAuthorBooks')}
data={translate('DataListMonitorAll')} data="Monitor authors and all books for each author included on the import list"
/> />
</DescriptionList> </DescriptionList>
); );
@@ -89,7 +89,7 @@ function EditImportListModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{id ? translate('EditList') : translate('AddList')} {id ? 'Edit List' : 'Add List'}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -148,7 +148,7 @@ function EditImportListModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('Monitor')} Monitor
<Popover <Popover
anchor={ anchor={
@@ -318,7 +318,7 @@ function EditImportListModalContent(props) {
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteImportListPress} onPress={onDeleteImportListPress}
> >
{translate('Delete')} Delete
</Button> </Button>
} }
@@ -327,13 +327,13 @@ function EditImportListModalContent(props) {
error={saveError} error={saveError}
onPress={onTestPress} onPress={onTestPress}
> >
{translate('Test')} Test
</SpinnerErrorButton> </SpinnerErrorButton>
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Cancel')} Cancel
</Button> </Button>
<SpinnerErrorButton <SpinnerErrorButton
@@ -341,7 +341,7 @@ function EditImportListModalContent(props) {
error={saveError} error={saveError}
onPress={onSavePress} onPress={onSavePress}
> >
{translate('Save')} Save
</SpinnerErrorButton> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

View File

@@ -346,7 +346,7 @@ function EditRootFolderModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('ConvertToFormat')} Convert to format
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -371,7 +371,7 @@ function EditRootFolderModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('CalibreOutputProfile')} Calibre Output Profile
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@@ -423,14 +423,14 @@ function EditRootFolderModalContent(props) {
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteRootFolderPress} onPress={onDeleteRootFolderPress}
> >
{translate('Delete')} Delete
</Button> </Button>
} }
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Cancel')} Cancel
</Button> </Button>
<SpinnerErrorButton <SpinnerErrorButton
@@ -438,7 +438,7 @@ function EditRootFolderModalContent(props) {
error={saveError} error={saveError}
onPress={onSavePress} onPress={onSavePress}
> >
{translate('Save')} Save
</SpinnerErrorButton> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

View File

@@ -78,8 +78,12 @@ export const actionHandlers = handleThunks({
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/history/failed/${historyId}`, url: '/history/failed',
method: 'POST' method: 'POST',
data: {
id: historyId
},
dataType: 'json'
}).request; }).request;
promise.done(() => { promise.done(() => {

View File

@@ -86,8 +86,12 @@ export const actionHandlers = handleThunks({
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/history/failed/${historyId}`, url: '/history/failed',
method: 'POST' method: 'POST',
data: {
id: historyId
},
dataType: 'json'
}).request; }).request;
promise.done(() => { promise.done(() => {

View File

@@ -1,8 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import getProviderState from 'Utilities/State/getProviderState';
import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer'; import createClearReducer from './Creators/Reducers/createClearReducer';
@@ -28,39 +25,18 @@ export const defaultState = {
export const FETCH_EDITIONS = 'editions/fetchEditions'; export const FETCH_EDITIONS = 'editions/fetchEditions';
export const CLEAR_EDITIONS = 'editions/clearEditions'; export const CLEAR_EDITIONS = 'editions/clearEditions';
export const SAVE_EDITIONS = 'editions/saveEditions';
// //
// Action Creators // Action Creators
export const fetchEditions = createThunk(FETCH_EDITIONS); export const fetchEditions = createThunk(FETCH_EDITIONS);
export const clearEditions = createAction(CLEAR_EDITIONS); export const clearEditions = createAction(CLEAR_EDITIONS);
export const saveEditions = createThunk(SAVE_EDITIONS);
// //
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_EDITIONS]: createFetchHandler(section, '/edition'), [FETCH_EDITIONS]: createFetchHandler(section, '/edition')
[SAVE_EDITIONS]: function(getState, payload, dispatch) {
const {
id,
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, 'books');
dispatch(batchActions([
...saveData.editions.map((edition) => {
return updateItem({
id: edition.id,
section: 'editions',
...edition
});
})
]));
}
}); });
// //

View File

@@ -246,8 +246,11 @@ export const actionHandlers = handleThunks({
})); }));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/history/failed/${id}`, url: '/history/failed',
method: 'POST', method: 'POST',
data: {
id
},
dataType: 'json' dataType: 'json'
}).request; }).request;

View File

@@ -177,8 +177,7 @@ export const defaultState = {
{ {
name: 'size', name: 'size',
label: 'Size', label: 'Size',
type: filterBuilderTypes.NUMBER, type: filterBuilderTypes.NUMBER
valueType: filterBuilderValueTypes.BYTES
}, },
{ {
name: 'seeders', name: 'seeders',

View File

@@ -1,15 +0,0 @@
import { createSelector } from 'reselect';
import createBookSelector from './createBookSelector';
function createBookAuthorSelector() {
return createSelector(
createBookSelector(),
(state) => state.authors.itemMap,
(state) => state.authors.items,
(book, authorMap, allAuthors) => {
return allAuthors[authorMap[book.authorId]];
}
);
}
export default createBookAuthorSelector;

View File

@@ -1,10 +1,10 @@
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector'; import createClientSideCollectionSelector from './createClientSideCollectionSelector';
function createUnoptimizedSelector(uiSection) { function createUnoptimizedSelector(uiSection) {
return createSelector( return createSelector(
createBooksClientSideCollectionSelector(uiSection), createClientSideCollectionSelector('books', uiSection),
(books) => { (books) => {
const items = books.items.map((s) => { const items = books.items.map((s) => {
const { const {

View File

@@ -1,16 +1,18 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createBookAuthorSelector from './createBookAuthorSelector'; import createBookSelector from './createBookSelector';
function createBookQualityProfileSelector() { function createBookQualityProfileSelector() {
return createSelector( return createSelector(
(state) => state.settings.qualityProfiles.items, (state) => state.settings.qualityProfiles.items,
createBookAuthorSelector(), createBookSelector(),
(qualityProfiles, author) => { (qualityProfiles, book) => {
if (!author) { if (!book) {
return {}; return {};
} }
return qualityProfiles.find((profile) => profile.id === author.qualityProfileId); return qualityProfiles.find((profile) => {
return profile.id === book.author.qualityProfileId;
});
} }
); );
} }

View File

@@ -1,35 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
import filterCollection from 'Utilities/Array/filterCollection';
import sortCollection from 'Utilities/Array/sortCollection';
import createCustomFiltersSelector from './createCustomFiltersSelector';
function createBooksClientSideCollectionSelector(uiSection) {
return createSelector(
(state) => _.get(state, 'books'),
(state) => _.get(state, 'authors'),
(state) => _.get(state, uiSection),
createCustomFiltersSelector('books', uiSection),
(bookState, authorState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, bookState, uiSectionState, { customFilters });
const books = state.items;
for (const book of books) {
book.author = authorState.items[authorState.itemMap[book.authorId]];
}
const filtered = filterCollection(books, state);
const sorted = sortCollection(filtered, state);
return {
...bookState,
...uiSectionState,
customFilters,
items: sorted,
totalItems: state.items.length
};
}
);
}
export default createBooksClientSideCollectionSelector;

View File

@@ -1,8 +1,123 @@
import _ from 'lodash'; import _ from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import filterCollection from 'Utilities/Array/filterCollection'; import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import sortCollection from 'Utilities/Array/sortCollection'; import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import createCustomFiltersSelector from './createCustomFiltersSelector';
function getSortClause(sortKey, sortDirection, sortPredicates) {
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
return function(item) {
return sortPredicates[sortKey](item, sortDirection);
};
}
return function(item) {
return item[sortKey];
};
}
function filter(items, state) {
const {
selectedFilterKey,
filters,
customFilters,
filterPredicates
} = state;
if (!selectedFilterKey) {
return items;
}
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
return _.filter(items, (item) => {
let i = 0;
let accepted = true;
while (accepted && i < selectedFilters.length) {
const {
key,
value,
type = filterTypes.EQUAL
} = selectedFilters[i];
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
const predicate = filterPredicates[key];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item, v, type));
} else {
accepted = value.some((v) => predicate(item, v, type));
}
} else {
accepted = predicate(item, value, type);
}
} else if (item.hasOwnProperty(key)) {
const predicate = filterTypePredicates[type];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item[key], v));
} else {
accepted = value.some((v) => predicate(item[key], v));
}
} else {
accepted = predicate(item[key], value);
}
} else {
// Default to false if the filter can't be tested
accepted = false;
}
i++;
}
return accepted;
});
}
function sort(items, state) {
const {
sortKey,
sortDirection,
sortPredicates,
secondarySortKey,
secondarySortDirection
} = state;
const clauses = [];
const orders = [];
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
if (secondarySortKey &&
secondarySortDirection &&
(sortKey !== secondarySortKey ||
sortDirection !== secondarySortDirection)) {
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
}
return _.orderBy(items, clauses, orders);
}
function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
function createClientSideCollectionSelector(section, uiSection) { function createClientSideCollectionSelector(section, uiSection) {
return createSelector( return createSelector(
@@ -12,8 +127,8 @@ function createClientSideCollectionSelector(section, uiSection) {
(sectionState, uiSectionState = {}, customFilters) => { (sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters }); const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
const filtered = filterCollection(state.items, state); const filtered = filter(state.items, state);
const sorted = sortCollection(filtered, state); const sorted = sort(filtered, state);
return { return {
...sectionState, ...sectionState,

View File

@@ -1,14 +0,0 @@
import { createSelector } from 'reselect';
function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
export default createCustomFiltersSelector;

View File

@@ -24,8 +24,6 @@ class About extends Component {
isDocker, isDocker,
runtimeVersion, runtimeVersion,
migrationVersion, migrationVersion,
databaseVersion,
databaseType,
appData, appData,
startupPath, startupPath,
mode, mode,
@@ -79,11 +77,6 @@ class About extends Component {
data={migrationVersion} data={migrationVersion}
/> />
<DescriptionListItem
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
/>
<DescriptionListItem <DescriptionListItem
title={translate('AppDataDirectory')} title={translate('AppDataDirectory')}
data={appData} data={appData}
@@ -125,8 +118,6 @@ About.propTypes = {
runtimeVersion: PropTypes.string.isRequired, runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired, migrationVersion: PropTypes.number.isRequired,
databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired,
appData: PropTypes.string.isRequired, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,

View File

@@ -1,72 +0,0 @@
import _ from 'lodash';
import { filterTypePredicates, filterTypes } from 'Helpers/Props';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
function filterCollection(items, state) {
const {
selectedFilterKey,
filters,
customFilters,
filterPredicates
} = state;
if (!selectedFilterKey) {
return items;
}
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
return _.filter(items, (item) => {
let i = 0;
let accepted = true;
while (accepted && i < selectedFilters.length) {
const {
key,
value,
type = filterTypes.EQUAL
} = selectedFilters[i];
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
const predicate = filterPredicates[key];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item, v, type));
} else {
accepted = value.some((v) => predicate(item, v, type));
}
} else {
accepted = predicate(item, value, type);
}
} else if (item.hasOwnProperty(key)) {
const predicate = filterTypePredicates[type];
if (Array.isArray(value)) {
if (
type === filterTypes.NOT_CONTAINS ||
type === filterTypes.NOT_EQUAL
) {
accepted = value.every((v) => predicate(item[key], v));
} else {
accepted = value.some((v) => predicate(item[key], v));
}
} else {
accepted = predicate(item[key], value);
}
} else {
// Default to false if the filter can't be tested
accepted = false;
}
i++;
}
return accepted;
});
}
export default filterCollection;

View File

@@ -1,42 +0,0 @@
import _ from 'lodash';
import { sortDirections } from 'Helpers/Props';
function getSortClause(sortKey, sortDirection, sortPredicates) {
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
return function(item) {
return sortPredicates[sortKey](item, sortDirection);
};
}
return function(item) {
return item[sortKey];
};
}
function sortCollection(items, state) {
const {
sortKey,
sortDirection,
sortPredicates,
secondarySortKey,
secondarySortDirection
} = state;
const clauses = [];
const orders = [];
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
if (secondarySortKey &&
secondarySortDirection &&
(sortKey !== secondarySortKey ||
sortDirection !== secondarySortDirection)) {
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
}
return _.orderBy(items, clauses, orders);
}
export default sortCollection;

View File

@@ -33,7 +33,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3", "@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14", "@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.7", "@microsoft/signalr": "6.0.3",
"@sentry/browser": "6.18.2", "@sentry/browser": "6.18.2",
"@sentry/integrations": "6.18.2", "@sentry/integrations": "6.18.2",
"ansi-colors": "4.1.1", "ansi-colors": "4.1.1",
@@ -134,8 +134,5 @@
"webpack-cli": "4.7.2", "webpack-cli": "4.7.2",
"webpack-livereload-plugin": "3.0.1", "webpack-livereload-plugin": "3.0.1",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
},
"volta": {
"node": "16.17.0"
} }
} }

View File

@@ -4,62 +4,60 @@
<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.2.0" /> <PackageVersion Include="DryIoc.dll" Version="4.8.7" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.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" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageVersion Include="FluentValidation" Version="8.6.2" /> <PackageVersion Include="FluentValidation" Version="8.6.2" />
<PackageVersion Include="Ical.Net" Version="4.2.0" /> <PackageVersion Include="Ical.Net" Version="4.2.0" />
<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.3.0" /> <PackageVersion Include="Mailkit" Version="3.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
<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.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" /> <PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr18" />
<PackageVersion Include="MonoTorrent" Version="2.0.4" />
<PackageVersion Include="Moq" Version="4.17.2" /> <PackageVersion Include="Moq" Version="4.17.2" />
<PackageVersion Include="MonoTorrent" Version="2.0.6" />
<PackageVersion Include="NBuilder" Version="6.1.0" /> <PackageVersion Include="NBuilder" Version="6.1.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageVersion Include="NLog" Version="4.7.14" /> <PackageVersion Include="NLog" Version="4.7.14" />
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
<PackageVersion Include="Npgsql" Version="6.0.3" />
<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.2" />
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
<PackageVersion Include="PdfSharpCore" Version="1.3.32" /> <PackageVersion Include="PdfSharpCore" Version="1.3.13" />
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" /> <PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
<PackageVersion Include="RestSharp" Version="106.15.0" /> <PackageVersion Include="RestSharp" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" /> <PackageVersion Include="Selenium.Support" Version="3.141.0" />
<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.20.1" /> <PackageVersion Include="Sentry" Version="3.14.1" />
<PackageVersion Include="SharpZipLib" Version="1.3.3" /> <PackageVersion Include="SharpZipLib" Version="1.3.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.3" /> <PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.5.1" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" /> <PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" /> <PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="16.1.20" />
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" /> <PackageVersion Include="System.IO.Abstractions" Version="16.1.20" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" /> <PackageVersion Include="System.Memory" Version="4.5.4" />
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<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.0" /> <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.5" /> <PackageVersion Include="System.Text.Json" Version="6.0.2" />
<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" />
<PackageVersion Include="Unity" Version="5.11.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,33 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" /> <add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" /> <add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" /> <add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" /> <add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
</packageSources> </packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet-bsd-crossbuild">
<package pattern="*" />
</packageSource>
<packageSource key="Mono.Posix.NETStandard">
<package pattern="Mono.Posix.NETStandard" />
</packageSource>
<packageSource key="SQLite">
<package pattern="System.Data.SQLite.Core.Servarr" />
</packageSource>
<packageSource key="coverlet-nightly">
<package pattern="coverlet.*" />
</packageSource>
<packageSource key="FluentMigrator">
<package pattern="Servarr.FluentMigrator*"/>
</packageSource>
</packageSourceMapping>
</configuration> </configuration>

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080); driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start();

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Abstractions;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -11,12 +10,6 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider where TSubject : class, IDiskProvider
{ {
[SetUp]
public void BaseSetup()
{
Mocker.SetConstant<IFileSystem>(new FileSystem());
}
[Test] [Test]
public void writealltext_should_truncate_existing() public void writealltext_should_truncate_existing()
{ {

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Abstractions;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@@ -11,12 +10,6 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject> public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider where TSubject : class, IDiskProvider
{ {
[SetUp]
public void BaseSetup()
{
Mocker.SetConstant<IFileSystem>(new FileSystem());
}
[Test] [Test]
public void should_get_free_space_for_folder() public void should_get_free_space_for_folder()
{ {

View File

@@ -1,4 +1,4 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@@ -60,7 +60,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")] [TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")] [TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")] [TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
// Announce URLs (passkeys) Magnet & Tracker // Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
@@ -71,11 +70,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")] [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")] [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Webhooks - Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
public void should_clean_message(string message) public void should_clean_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
@@ -86,7 +80,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
//GoodReads //GoodReads
[TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")] [TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")]
[TestCase(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
public void should_cleanGoodRead_message(string message) public void should_cleanGoodRead_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);

View File

@@ -4,13 +4,11 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -31,8 +29,7 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase() .AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second")); .AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance(new Mock<IHostLifetime>().Object); container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
var serviceProvider = container.GetServiceProvider(); var serviceProvider = container.GetServiceProvider();
serviceProvider.GetRequiredService<IAppFolderFactory>().Register(); serviceProvider.GetRequiredService<IAppFolderFactory>().Register();

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -11,14 +11,13 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[] private static readonly Regex[] CleansingRules = new[]
{ {
// Url // Url
new Regex(@"(?<=\?|&|: )((?:api|auth|pass)?key|(?:access[-_]?)?token|auth|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"), new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
@@ -47,11 +46,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Good Reads // Good Reads
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase)
// Webhooks
// Notifiarr
new Regex(@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
}; };
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled); private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);

View File

@@ -127,18 +127,7 @@ namespace NzbDrone.Common.Processes
try try
{ {
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value); _logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
var key = environmentVariable.Key.ToString();
var value = environmentVariable.Value?.ToString();
if (startInfo.EnvironmentVariables.ContainsKey(key))
{
startInfo.EnvironmentVariables[key] = value;
}
else
{
startInfo.EnvironmentVariables.Add(key, value);
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
public void SingleOrDefault_should_return_null_on_empty_db() public void SingleOrDefault_should_return_null_on_empty_db()
{ {
Mocker.Resolve<IDatabase>() Mocker.Resolve<IDatabase>()
.OpenConnection().Query<Author>("SELECT * FROM \"Authors\"") .OpenConnection().Query<Author>("SELECT * FROM Authors")
.SingleOrDefault(c => c.CleanName == "SomeTitle") .SingleOrDefault(c => c.CleanName == "SomeTitle")
.Should() .Should()
.BeNull(); .BeNull();

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_author_for_trackfile() public void should_lazy_load_author_for_trackfile()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).ToList(); var tracks = db.Query<BookFile>(new SqlBuilder()).ToList();
Assert.IsNotEmpty(tracks); Assert.IsNotEmpty(tracks);
foreach (var track in tracks) foreach (var track in tracks)
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_trackfile_if_not_joined() public void should_lazy_load_trackfile_if_not_joined()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).ToList(); var tracks = db.Query<Book>(new SqlBuilder()).ToList();
foreach (var track in tracks) foreach (var track in tracks)
{ {
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Datastore
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var files = MediaFileRepository.Query(db, var files = MediaFileRepository.Query(db,
new SqlBuilder(db.DatabaseType) new SqlBuilder()
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id) .Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
.Join<Edition, Book>((e, b) => e.BookId == b.Id) .Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId) .Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)

View File

@@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore namespace NzbDrone.Core.Test.Datastore
{ {
[TestFixture] [TestFixture]
public class WhereBuilderSqliteFixture : CoreTest public class WhereBuilderFixture : CoreTest
{ {
private WhereBuilderSqlite _subject; private WhereBuilder _subject;
[OneTimeSetUp] [OneTimeSetUp]
public void MapTables() public void MapTables()
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
Mocker.Resolve<DbFactory>(); Mocker.Resolve<DbFactory>();
} }
private WhereBuilderSqlite Where(Expression<Func<Author, bool>> filter) private WhereBuilder Where(Expression<Func<Author, bool>> filter)
{ {
return new WhereBuilderSqlite(filter, true, 0); return new WhereBuilder(filter, true, 0);
} }
private WhereBuilderSqlite WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter) private WhereBuilder WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
{ {
return new WhereBuilderSqlite(filter, true, 0); return new WhereBuilder(filter, true, 0);
} }
[Test] [Test]
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_throws_without_concrete_condition_if_requiresConcreteCondition() public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
{ {
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderSqlite(filter, true, 0); _subject = new WhereBuilder(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString()); Assert.Throws<InvalidOperationException>(() => _subject.ToString());
} }
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_allows_abstract_condition_if_not_requiresConcreteCondition() public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
{ {
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderSqlite(filter, false, 0); _subject = new WhereBuilder(filter, false, 0);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")"); _subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
} }

View File

@@ -1,211 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore
{
[TestFixture]
public class WhereBuilderPostgresFixture : CoreTest
{
private WhereBuilderPostgres _subject;
[OneTimeSetUp]
public void MapTables()
{
// Generate table mapping
Mocker.Resolve<DbFactory>();
}
private WhereBuilderPostgres Where(Expression<Func<Author, bool>> filter)
{
return new WhereBuilderPostgres(filter, true, 0);
}
private WhereBuilderPostgres WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
{
return new WhereBuilderPostgres(filter, true, 0);
}
[Test]
public void postgres_where_equal_const()
{
_subject = Where(x => x.Id == 10);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(10);
}
[Test]
public void postgres_where_equal_variable()
{
var id = 10;
_subject = Where(x => x.Id == id);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(id);
}
[Test]
public void postgres_where_equal_property()
{
var author = new Author { Id = 10 };
_subject = Where(x => x.Id == author.Id);
_subject.Parameters.ParameterNames.Should().HaveCount(1);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(author.Id);
}
[Test]
public void postgres_where_equal_joined_property()
{
_subject = Where(x => x.QualityProfile.Value.Id == 1);
_subject.Parameters.ParameterNames.Should().HaveCount(1);
_subject.ToString().Should().Be($"(\"QualityProfiles\".\"Id\" = @Clause1_P1)");
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(1);
}
[Test]
public void postgres_where_throws_without_concrete_condition_if_requiresConcreteCondition()
{
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderPostgres(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
}
[Test]
public void postgres_where_allows_abstract_condition_if_not_requiresConcreteCondition()
{
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderPostgres(filter, false, 0);
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
}
[Test]
public void postgres_where_string_is_null()
{
_subject = Where(x => x.CleanName == null);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_string_is_null_value()
{
string cleanName = null;
_subject = Where(x => x.CleanName == cleanName);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_equal_null_property()
{
var author = new Author { CleanName = null };
_subject = Where(x => x.CleanName == author.CleanName);
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
}
[Test]
public void postgres_where_column_contains_string()
{
var test = "small";
_subject = Where(x => x.CleanName.Contains(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_string_contains_column()
{
var test = "small";
_subject = Where(x => test.Contains(x.CleanName));
_subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Authors\".\"CleanName\" || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_column_starts_with_string()
{
var test = "small";
_subject = Where(x => x.CleanName.StartsWith(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE @Clause1_P1 || '%')");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_column_ends_with_string()
{
var test = "small";
_subject = Where(x => x.CleanName.EndsWith(test));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1)");
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void postgres_where_in_list()
{
var list = new List<int> { 1, 2, 3 };
_subject = Where(x => list.Contains(x.Id));
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}')))");
}
[Test]
public void postgres_where_in_list_2()
{
var list = new List<int> { 1, 2, 3 };
_subject = Where(x => x.CleanName == "test" && list.Contains(x.Id));
_subject.ToString().Should().Be($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}'))))");
}
[Test]
public void postgres_where_in_string_list()
{
var list = new List<string> { "first", "second", "third" };
_subject = Where(x => list.Contains(x.CleanName));
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" = ANY (@Clause1_P1))");
}
[Test]
public void enum_as_int()
{
_subject = WhereMetadata(x => x.Status == AuthorStatusType.Continuing);
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)");
}
[Test]
public void enum_in_list()
{
var allowed = new List<AuthorStatusType> { AuthorStatusType.Continuing, AuthorStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status));
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
}
[Test]
public void enum_in_array()
{
var allowed = new AuthorStatusType[] { AuthorStatusType.Continuing, AuthorStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status));
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
}
}
}

View File

@@ -1,18 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
{ {
@@ -53,7 +47,6 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest public abstract class DbTest : CoreTest
{ {
private ITestDatabase _db; private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main; protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -72,7 +65,8 @@ namespace NzbDrone.Core.Test.Framework
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext) protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
{ {
var database = CreateDatabase(migrationContext); var factory = Mocker.Resolve<DbFactory>();
var database = factory.Create(migrationContext);
Mocker.SetConstant(database); Mocker.SetConstant(database);
switch (MigrationType) switch (MigrationType)
@@ -104,65 +98,6 @@ namespace NzbDrone.Core.Test.Framework
return testDb; return testDb;
} }
private IDatabase CreateDatabase(MigrationContext migrationContext)
{
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
{
return factory.Create(migrationContext);
}
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
if (File.Exists(cachedDb))
{
TestLogger.Info($"Using cached initial database {cachedDb}");
File.Copy(cachedDb, testDb);
return factory.Create(migrationContext);
}
else
{
var db = factory.Create(migrationContext);
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
TestLogger.Info("Caching database");
File.Copy(testDb, cachedDb);
return db;
}
}
private string GetTestSqliteDb(MigrationType type)
{
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
}
protected virtual void SetupLogging() protected virtual void SetupLogging()
{ {
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance); Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
@@ -173,13 +108,6 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath(); WithTempAsAppPath();
SetupLogging(); SetupLogging();
// populate the possible postgres options
var postgresOptions = PostgresDatabase.GetTestOptions();
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
// Set up remaining container services
Mocker.SetConstant(Options.Create(postgresOptions));
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>()); Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>()); Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@@ -199,19 +127,12 @@ namespace NzbDrone.Core.Test.Framework
// Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly) // Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
GC.Collect(); GC.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools(); SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null) if (TestFolderInfo != null)
{ {
DeleteTempFolder(TestFolderInfo.AppDataFolder); DeleteTempFolder(TestFolderInfo.AppDataFolder);
} }
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
} }
} }
} }

View File

@@ -1,8 +1,7 @@
using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers;
using System.IO.Abstractions.TestingHelpers;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Test.Common.AutoMoq; using Unity.Resolution;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
{ {
@@ -15,9 +14,12 @@ namespace NzbDrone.Core.Test.Framework
[SetUp] [SetUp]
public void FileSystemTestSetup() public void FileSystemTestSetup()
{ {
FileSystem = (MockFileSystem)Mocker.Resolve<IFileSystem>(FileSystemType.Mock); FileSystem = new MockFileSystem();
DiskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Mock); DiskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider", new ResolverOverride[]
{
new ParameterOverride("fileSystem", FileSystem)
});
} }
} }
} }

View File

@@ -23,7 +23,6 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new(); where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper(); IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection(); IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
} }
public class TestDatabase : ITestDatabase public class TestDatabase : ITestDatabase
@@ -31,8 +30,6 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection; private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection) public TestDatabase(IDatabase dbConnection)
{ {
_eventAggregator = new Mock<IEventAggregator>().Object; _eventAggregator = new Mock<IEventAggregator>().Object;

View File

@@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
@@ -135,13 +133,6 @@ namespace NzbDrone.Core.Test.MediaCoverTests
} }
}; };
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "NonExistant.mp4");
var fileInfo = new FileInfo(path);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFileInfo(It.IsAny<string>()))
.Returns((FileInfoBase)fileInfo);
Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers); Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers);
covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension); covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension);

View File

@@ -13,7 +13,6 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
{ {
@@ -56,7 +55,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual); _diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider");
Mocker.SetConstant<IDiskProvider>(_diskProvider);
Mocker.GetMock<IConfigService>() Mocker.GetMock<IConfigService>()
.Setup(x => x.WriteAudioTags) .Setup(x => x.WriteAudioTags)

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
} }
[TestCase("Harry Potter and the sorcerer's stone", 3)] [TestCase("Harry Potter and the sorcerer's stone", 3)]
[TestCase("B0192CTMYG", 61209488)] [TestCase("B0192CTMYG", 42844155)]
[TestCase("9780439554930", 48517161)] [TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected) public void successful_book_search(string title, int expected)
{ {

View File

@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using FluentAssertions.Equivalency;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
@@ -23,13 +21,6 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
AssertionOptions.AssertEquivalencyUsing(options =>
{
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
return options;
});
_author = new Author _author = new Author
{ {
Name = "Alien Ant Farm", Name = "Alien Ant Farm",
@@ -152,7 +143,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks(); GivenMultipleBooks();
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId }); var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions); result.Should().BeEquivalentTo(_books.Take(1));
} }
[Test] [Test]
@@ -161,11 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks(); GivenMultipleBooks();
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId }); var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
result.Should().BeEquivalentTo(_books.Skip(2).Take(1), BookComparerOptions); result.Should().BeEquivalentTo(_books.Skip(2).Take(1));
} }
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
.Excluding(x => x.AuthorId);
} }
} }

View File

@@ -3,11 +3,9 @@ using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@@ -147,14 +145,7 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
_authorRepo.Insert(author1); _authorRepo.Insert(author1);
Action insertDupe = () => _authorRepo.Insert(author2); Action insertDupe = () => _authorRepo.Insert(author2);
if (Db.DatabaseType == DatabaseType.PostgreSQL) insertDupe.Should().Throw<SQLiteException>();
{
insertDupe.Should().Throw<PostgresException>();
}
else
{
insertDupe.Should().Throw<SQLiteException>();
}
} }
} }
} }

View File

@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenBooksForRefresh(List<Book> books) private void GivenBooksForRefresh(List<Book> books)
{ {
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>())) .Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(books); .Returns(books);
} }
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>())) .Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(new List<Book>()); .Returns(new List<Book>());
// Update called twice for a move/merge // Update called twice for a move/merge
@@ -298,7 +298,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IBookService>(MockBehavior.Strict) Mocker.GetMock<IBookService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>())) .Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<IEnumerable<string>>()))
.Returns(_books); .Returns(_books);
// Update called twice for a move/merge // Update called twice for a move/merge

View File

@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test] [Test]
public void finds_update_when_version_lower() public void finds_update_when_version_lower()
{ {
NotBsd();
UseRealHttp(); UseRealHttp();
Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull(); Subject.GetLatestUpdate("nightly", new Version(0, 1)).Should().NotBeNull();
} }
@@ -42,6 +43,8 @@ namespace NzbDrone.Core.Test.UpdateTests
[Test] [Test]
public void should_get_recent_updates() public void should_get_recent_updates()
{ {
NotBsd();
const string branch = "nightly"; const string branch = "nightly";
UseRealHttp(); UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(0, 1)); var recent = Subject.GetRecentUpdates(branch, new Version(0, 1));

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
public class AuthorStatisticsRepository : IAuthorStatisticsRepository public class AuthorStatisticsRepository : IAuthorStatisticsRepository
{ {
private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private readonly IMainDatabase _database; private readonly IMainDatabase _database;
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
} }
} }
private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType) private SqlBuilder Builder() => new SqlBuilder()
.Select(@"""Authors"".""Id"" AS ""AuthorId"", .Select(@"Authors.Id AS AuthorId,
""Books"".""Id"" AS ""BookId"", Books.Id AS BookId,
SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"", SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk,
1 AS ""TotalBookCount"", 1 AS TotalBookCount,
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE 1 END AS ""AvailableBookCount"", CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount,
CASE WHEN (""Books"".""Monitored"" = true AND (""Books"".""ReleaseDate"" < @currentDate) OR ""Books"".""ReleaseDate"" IS NULL) OR MIN(""BookFiles"".""Id"") IS NOT NULL THEN 1 ELSE 0 END AS ""BookCount"", CASE WHEN (Books.Monitored = 1 AND (Books.ReleaseDate < @currentDate) OR Books.ReleaseDate IS NULL) OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END AS BookCount,
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""") CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount")
.Join<Edition, Book>((e, b) => e.BookId == b.Id) .Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId) .Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId) .LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)

View File

@@ -15,14 +15,12 @@ namespace NzbDrone.Core.AuthorStats
} }
public class AuthorStatisticsService : IAuthorStatisticsService, public class AuthorStatisticsService : IAuthorStatisticsService,
IHandle<AuthorAddedEvent>,
IHandle<AuthorUpdatedEvent>, IHandle<AuthorUpdatedEvent>,
IHandle<AuthorDeletedEvent>, IHandle<AuthorDeletedEvent>,
IHandle<BookAddedEvent>, IHandle<BookAddedEvent>,
IHandle<BookDeletedEvent>, IHandle<BookDeletedEvent>,
IHandle<BookImportedEvent>, IHandle<BookImportedEvent>,
IHandle<BookEditedEvent>, IHandle<BookEditedEvent>,
IHandle<BookUpdatedEvent>,
IHandle<BookFileDeletedEvent> IHandle<BookFileDeletedEvent>
{ {
private readonly IAuthorStatisticsRepository _authorStatisticsRepository; private readonly IAuthorStatisticsRepository _authorStatisticsRepository;
@@ -70,13 +68,6 @@ namespace NzbDrone.Core.AuthorStats
return authorStatistics; return authorStatistics;
} }
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(AuthorAddedEvent message)
{
_cache.Remove("AllAuthors");
_cache.Remove(message.Author.Id.ToString());
}
[EventHandleOrder(EventHandleOrder.First)] [EventHandleOrder(EventHandleOrder.First)]
public void Handle(AuthorUpdatedEvent message) public void Handle(AuthorUpdatedEvent message)
{ {
@@ -119,13 +110,6 @@ namespace NzbDrone.Core.AuthorStats
_cache.Remove(message.Book.AuthorId.ToString()); _cache.Remove(message.Book.AuthorId.ToString());
} }
[EventHandleOrder(EventHandleOrder.First)]
public void Handle(BookUpdatedEvent message)
{
_cache.Remove("AllAuthors");
_cache.Remove(message.Book.AuthorId.ToString());
}
[EventHandleOrder(EventHandleOrder.First)] [EventHandleOrder(EventHandleOrder.First)]
public void Handle(BookFileDeletedEvent message) public void Handle(BookFileDeletedEvent message)
{ {

View File

@@ -183,12 +183,9 @@ namespace NzbDrone.Core.Backup
private void BackupDatabase() private void BackupDatabase()
{ {
if (_maindDb.DatabaseType == DatabaseType.SQLite) _logger.ProgressDebug("Backing up database");
{
_logger.ProgressDebug("Backing up database");
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder); _makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
}
} }
private void BackupConfigFile() private void BackupConfigFile()

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Blocklisting
return Query(b => b.AuthorId == authorId); return Query(b => b.AuthorId == authorId);
} }
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType) protected override SqlBuilder PagedBuilder() => new SqlBuilder()
.Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id) .Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id)
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id); .Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder, protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder,

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
{ {
} }
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType) protected override SqlBuilder Builder() => new SqlBuilder()
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id); .Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id);
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList(); protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Books
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Authors\""; var strSql = "SELECT Id AS [Key], Path AS [Value] FROM Authors";
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value); return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
} }
} }

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Books
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds); List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds); List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId); List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds); List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds); List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
Book FindByTitle(int authorMetadataId, string title); Book FindByTitle(int authorMetadataId, string title);
Book FindById(string foreignBookId); Book FindById(string foreignBookId);
@@ -44,35 +44,17 @@ namespace NzbDrone.Core.Books
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds) public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
var inner = Builder() .GroupBy<Book>(x => x.AuthorMetadataId)
.Select("MIN(\"Books\".\"Id\") as id, MAX(\"Books\".\"ReleaseDate\") as date") .Having("Books.ReleaseDate = MAX(Books.ReleaseDate)"));
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
.GroupBy<Book>(x => x.AuthorMetadataId)
.AddSelectTemplate(typeof(Book));
var outer = Builder()
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
.AddParameters(inner.Parameters);
return Query(outer);
} }
public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds) public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
var inner = Builder() .GroupBy<Book>(x => x.AuthorMetadataId)
.Select("MIN(\"Books\".\"Id\") as id, MIN(\"Books\".\"ReleaseDate\") as date") .Having("Books.ReleaseDate = MIN(Books.ReleaseDate)"));
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
.GroupBy<Book>(x => x.AuthorMetadataId)
.AddSelectTemplate(typeof(Book));
var outer = Builder()
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
.AddParameters(inner.Parameters);
return Query(outer);
} }
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId) public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
@@ -80,14 +62,14 @@ namespace NzbDrone.Core.Books
return Query(s => s.AuthorMetadataId == authorMetadataId); return Query(s => s.AuthorMetadataId == authorMetadataId);
} }
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds) public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
{ {
return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId)); return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId));
} }
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds) public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
{ {
return Query(new SqlBuilder(_database.DatabaseType) return Query(new SqlBuilder()
.Join<Book, Edition>((b, e) => b.Id == e.BookId) .Join<Book, Edition>((b, e) => b.Id == e.BookId)
.Join<Edition, BookFile>((l, r) => l.Id == r.EditionId) .Join<Edition, BookFile>((l, r) => l.Id == r.EditionId)
.Where<BookFile>(f => fileIds.Contains(f.Id))) .Where<BookFile>(f => fileIds.Contains(f.Id)))
@@ -143,7 +125,7 @@ namespace NzbDrone.Core.Books
{ {
foreach (var belowCutoff in profile.QualityIds) foreach (var belowCutoff in profile.QualityIds)
{ {
clauses.Add(string.Format("(\"Authors\".\"QualityProfileId\" = {0} AND \"BookFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); clauses.Add(string.Format("(Authors.[QualityProfileId] = {0} AND BookFiles.Quality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
} }
} }
@@ -154,7 +136,7 @@ namespace NzbDrone.Core.Books
{ {
pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery); pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Book))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\""; var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Book))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)";
pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate); pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate);
return pagingSpec; return pagingSpec;

View File

@@ -11,11 +11,11 @@ namespace NzbDrone.Core.Books
{ {
List<Edition> GetAllMonitoredEditions(); List<Edition> GetAllMonitoredEditions();
Edition FindByForeignEditionId(string foreignEditionId); Edition FindByForeignEditionId(string foreignEditionId);
List<Edition> FindByBook(IEnumerable<int> ids); List<Edition> FindByBook(int id);
List<Edition> FindByAuthor(int id); List<Edition> FindByAuthor(int id);
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored); List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
Edition FindByTitle(int authorMetadataId, string title); Edition FindByTitle(int authorMetadataId, string title);
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds); List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
List<Edition> SetMonitored(Edition edition); List<Edition> SetMonitored(Edition edition);
} }
@@ -38,19 +38,19 @@ namespace NzbDrone.Core.Books
return edition; return edition;
} }
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds) public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
{ {
return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId)); return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId));
} }
public List<Edition> FindByBook(IEnumerable<int> ids) public List<Edition> FindByBook(int id)
{ {
// populate the books and author metadata also // populate the books and author metadata also
// this hopefully speeds up the track matching a lot // this hopefully speeds up the track matching a lot
var builder = new SqlBuilder(_database.DatabaseType) var builder = new SqlBuilder()
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id) .LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id) .LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
.Where<Edition>(r => ids.Contains(r.BookId)); .Where<Edition>(r => r.BookId == id);
return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) => return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) =>
{ {
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Books
public List<Edition> SetMonitored(Edition edition) public List<Edition> SetMonitored(Edition edition)
{ {
var allEditions = FindByBook(new[] { edition.BookId }); var allEditions = FindByBook(edition.BookId);
allEditions.ForEach(r => r.Monitored = r.Id == edition.Id); allEditions.ForEach(r => r.Monitored = r.Id == edition.Id);
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue(); Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
UpdateMany(allEditions); UpdateMany(allEditions);

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Books
public interface ISeriesRepository : IBasicRepository<Series> public interface ISeriesRepository : IBasicRepository<Series>
{ {
Series FindById(string foreignSeriesId); Series FindById(string foreignSeriesId);
List<Series> FindById(List<string> foreignSeriesId); List<Series> FindById(IEnumerable<string> foreignSeriesId);
List<Series> GetByAuthorMetadataId(int authorMetadataId); List<Series> GetByAuthorMetadataId(int authorMetadataId);
List<Series> GetByAuthorId(int authorId); List<Series> GetByAuthorId(int authorId);
} }
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault(); return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault();
} }
public List<Series> FindById(List<string> foreignSeriesId) public List<Series> FindById(IEnumerable<string> foreignSeriesId)
{ {
return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId)); return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId));
} }

View File

@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Books
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds); List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds); List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId); List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds); List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds); List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
Book AddBook(Book newBook, bool doRefresh = true); Book AddBook(Book newBook, bool doRefresh = true);
Book FindById(string foreignId); Book FindById(string foreignId);
@@ -206,7 +206,7 @@ namespace NzbDrone.Core.Books
return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList(); return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList();
} }
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds) public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
{ {
return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds); return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds);
} }

View File

@@ -16,9 +16,8 @@ namespace NzbDrone.Core.Books
void InsertMany(List<Edition> editions); void InsertMany(List<Edition> editions);
void UpdateMany(List<Edition> editions); void UpdateMany(List<Edition> editions);
void DeleteMany(List<Edition> editions); void DeleteMany(List<Edition> editions);
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds); List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
List<Edition> GetEditionsByBook(int bookId); List<Edition> GetEditionsByBook(int bookId);
List<Edition> GetEditionsByBook(IEnumerable<int> bookIds);
List<Edition> GetEditionsByAuthor(int authorId); List<Edition> GetEditionsByAuthor(int authorId);
Edition FindByTitle(int authorMetadataId, string title); Edition FindByTitle(int authorMetadataId, string title);
Edition FindByTitleInexact(int authorMetadataId, string title); Edition FindByTitleInexact(int authorMetadataId, string title);
@@ -73,19 +72,14 @@ namespace NzbDrone.Core.Books
} }
} }
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds) public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
{ {
return _editionRepository.GetEditionsForRefresh(bookId, foreignEditionIds); return _editionRepository.GetEditionsForRefresh(bookId, foreignEditionIds);
} }
public List<Edition> GetEditionsByBook(int bookId) public List<Edition> GetEditionsByBook(int bookId)
{ {
return _editionRepository.FindByBook(new[] { bookId }); return _editionRepository.FindByBook(bookId);
}
public List<Edition> GetEditionsByBook(IEnumerable<int> bookIds)
{
return _editionRepository.FindByBook(bookIds);
} }
public List<Edition> GetEditionsByAuthor(int authorId) public List<Edition> GetEditionsByAuthor(int authorId)

View File

@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Books
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren) protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
{ {
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId, return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
remoteChildren.Select(x => x.ForeignBookId).ToList()); remoteChildren.Select(x => x.ForeignBookId));
} }
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote) protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)

View File

@@ -14,7 +14,6 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Books namespace NzbDrone.Core.Books
{ {
@@ -31,7 +30,6 @@ namespace NzbDrone.Core.Books
{ {
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IRootFolderService _rootFolderService;
private readonly IAddAuthorService _addAuthorService; private readonly IAddAuthorService _addAuthorService;
private readonly IEditionService _editionService; private readonly IEditionService _editionService;
private readonly IProvideAuthorInfo _authorInfo; private readonly IProvideAuthorInfo _authorInfo;
@@ -46,7 +44,6 @@ namespace NzbDrone.Core.Books
public RefreshBookService(IBookService bookService, public RefreshBookService(IBookService bookService,
IAuthorService authorService, IAuthorService authorService,
IRootFolderService rootFolderService,
IAddAuthorService addAuthorService, IAddAuthorService addAuthorService,
IEditionService editionService, IEditionService editionService,
IAuthorMetadataService authorMetadataService, IAuthorMetadataService authorMetadataService,
@@ -63,7 +60,6 @@ namespace NzbDrone.Core.Books
{ {
_bookService = bookService; _bookService = bookService;
_authorService = authorService; _authorService = authorService;
_rootFolderService = rootFolderService;
_addAuthorService = addAuthorService; _addAuthorService = addAuthorService;
_editionService = editionService; _editionService = editionService;
_authorInfo = authorInfo; _authorInfo = authorInfo;
@@ -146,7 +142,7 @@ namespace NzbDrone.Core.Books
Metadata = remote.AuthorMetadata.Value, Metadata = remote.AuthorMetadata.Value,
MetadataProfileId = oldAuthor.MetadataProfileId, MetadataProfileId = oldAuthor.MetadataProfileId,
QualityProfileId = oldAuthor.QualityProfileId, QualityProfileId = oldAuthor.QualityProfileId,
RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldAuthor.Path), RootFolderPath = oldAuthor.RootFolderPath,
Monitored = oldAuthor.Monitored, Monitored = oldAuthor.Monitored,
Tags = oldAuthor.Tags Tags = oldAuthor.Tags
}; };
@@ -250,7 +246,7 @@ namespace NzbDrone.Core.Books
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren) protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
{ {
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId).ToList()); return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId));
} }
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote) protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)

View File

@@ -132,7 +132,7 @@ namespace NzbDrone.Core.Books
var updated = false; var updated = false;
var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId); var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId);
var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId).ToList()); var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId));
var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList(); var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList();
var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId); var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);

View File

@@ -5,7 +5,7 @@ namespace NzbDrone.Core.Books
public interface ISeriesService public interface ISeriesService
{ {
Series FindById(string foreignSeriesId); Series FindById(string foreignSeriesId);
List<Series> FindById(List<string> foreignSeriesId); List<Series> FindById(IEnumerable<string> foreignSeriesId);
List<Series> GetByAuthorMetadataId(int authorMetadataId); List<Series> GetByAuthorMetadataId(int authorMetadataId);
List<Series> GetByAuthorId(int authorId); List<Series> GetByAuthorId(int authorId);
void Delete(int seriesId); void Delete(int seriesId);
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Books
return _seriesRepository.FindById(foreignSeriesId); return _seriesRepository.FindById(foreignSeriesId);
} }
public List<Series> FindById(List<string> foreignSeriesId) public List<Series> FindById(IEnumerable<string> foreignSeriesId)
{ {
return _seriesRepository.FindById(foreignSeriesId); return _seriesRepository.FindById(foreignSeriesId);
} }

View File

@@ -4,14 +4,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -20,7 +18,7 @@ using NzbDrone.Core.Update;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
{ {
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>, public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
IExecute<ResetApiKeyCommand> IExecute<ResetApiKeyCommand>
{ {
Dictionary<string, object> GetConfigDictionary(); Dictionary<string, object> GetConfigDictionary();
void SaveConfigDictionary(Dictionary<string, object> configValues); void SaveConfigDictionary(Dictionary<string, object> configValues);
@@ -43,20 +41,9 @@ namespace NzbDrone.Core.Configuration
string SslCertPassword { get; } string SslCertPassword { get; }
string UrlBase { get; } string UrlBase { get; }
string UiFolder { get; } string UiFolder { get; }
string InstanceName { get; }
bool UpdateAutomatically { get; } bool UpdateAutomatically { get; }
UpdateMechanism UpdateMechanism { get; } UpdateMechanism UpdateMechanism { get; }
string UpdateScriptPath { get; } string UpdateScriptPath { get; }
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
string PostgresHost { get; }
int PostgresPort { get; }
string PostgresUser { get; }
string PostgresPassword { get; }
string PostgresMainDb { get; }
string PostgresLogDb { get; }
string PostgresCacheDb { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
@@ -66,7 +53,6 @@ namespace NzbDrone.Core.Configuration
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly string _configFile; private readonly string _configFile;
@@ -75,14 +61,12 @@ namespace NzbDrone.Core.Configuration
public ConfigFileProvider(IAppFolderInfo appFolderInfo, public ConfigFileProvider(IAppFolderInfo appFolderInfo,
ICacheManager cacheManager, ICacheManager cacheManager,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IDiskProvider diskProvider, IDiskProvider diskProvider)
IOptions<PostgresOptions> postgresOptions)
{ {
_cache = cacheManager.GetCache<string>(GetType()); _cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath(); _configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
} }
public Dictionary<string, object> GetConfigDictionary() public Dictionary<string, object> GetConfigDictionary()
@@ -196,13 +180,6 @@ namespace NzbDrone.Core.Configuration
public string LogLevel => GetValue("LogLevel", "info"); public string LogLevel => GetValue("LogLevel", "info");
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "readarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "readarr-log", persist: false);
public string PostgresCacheDb => _postgresOptions?.CacheDb ?? GetValue("PostgresCacheDb", "readarr-cache", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false); public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false); public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
@@ -225,7 +202,6 @@ namespace NzbDrone.Core.Configuration
} }
public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI"; public string UiFolder => BuildInfo.IsDebug ? Path.Combine("..", "UI") : "UI";
public string InstanceName => GetValue("InstanceName", BuildInfo.AppName);
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false); public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
@@ -233,12 +209,6 @@ namespace NzbDrone.Core.Configuration
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false); public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, false).ToLowerInvariant();
public int GetValueInt(string key, int defaultValue, bool persist = true) public int GetValueInt(string key, int defaultValue, bool persist = true)
{ {
return Convert.ToInt32(GetValue(key, defaultValue, persist)); return Convert.ToInt32(GetValue(key, defaultValue, persist));

View File

@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Datastore
_updateSql = GetUpdateSql(_properties); _updateSql = GetUpdateSql(_properties);
} }
protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType); protected virtual SqlBuilder Builder() => new SqlBuilder();
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList(); protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();
@@ -80,7 +80,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM \"{_table}\""); return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}");
} }
} }
@@ -176,11 +176,6 @@ namespace NzbDrone.Core.Datastore
} }
} }
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
}
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id"; return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
} }
@@ -199,8 +194,7 @@ namespace NzbDrone.Core.Datastore
throw; throw;
} }
var multiRead = multi.Read(); var id = (int)multi.Read().First().id;
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
_keyProperty.SetValue(model, id); _keyProperty.SetValue(model, id);
return model; return model;
@@ -311,7 +305,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
conn.Execute($"DELETE FROM \"{_table}\""); conn.Execute($"DELETE FROM [{_table}]");
} }
if (vacuum) if (vacuum)
@@ -370,7 +364,7 @@ namespace NzbDrone.Core.Datastore
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate) private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendFormat("UPDATE \"{0}\" SET ", _table); sb.AppendFormat("UPDATE {0} SET ", _table);
for (var i = 0; i < propertiesToUpdate.Count; i++) for (var i = 0; i < propertiesToUpdate.Count; i++)
{ {
@@ -452,10 +446,9 @@ namespace NzbDrone.Core.Datastore
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}"; pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
} }
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC"; var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize; var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}"); builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
return queryFunc(builder).ToList(); return queryFunc(builder).ToList();
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Data; using System.Data;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
@@ -10,12 +10,10 @@ namespace NzbDrone.Core.Datastore
public class CacheDatabase : ICacheDatabase public class CacheDatabase : ICacheDatabase
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public CacheDatabase(IDatabase database) public CacheDatabase(IDatabase database)
{ {
_database = database; _database = database;
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
} }
public IDbConnection OpenConnection() public IDbConnection OpenConnection()
@@ -27,8 +25,6 @@ namespace NzbDrone.Core.Datastore
public int Migration => _database.Migration; public int Migration => _database.Migration;
public DatabaseType DatabaseType => _databaseType;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

View File

@@ -1,9 +1,7 @@
using System; using System;
using System.Data.SQLite; using System.Data.SQLite;
using Npgsql;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
@@ -17,20 +15,11 @@ namespace NzbDrone.Core.Datastore
public class ConnectionStringFactory : IConnectionStringFactory public class ConnectionStringFactory : IConnectionStringFactory
{ {
private readonly IConfigFileProvider _configFileProvider; public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
{ {
_configFileProvider = configFileProvider; MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : CacheDbConnectionString = GetConnectionString(appFolderInfo.GetCacheDatabase());
GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
GetConnectionString(appFolderInfo.GetLogDatabase());
CacheDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
GetConnectionString(appFolderInfo.GetCacheDatabase());
} }
public string MainDbConnectionString { get; private set; } public string MainDbConnectionString { get; private set; }
@@ -62,19 +51,5 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.ConnectionString; return connectionBuilder.ConnectionString;
} }
private string GetPostgresConnectionString(string dbName)
{
var connectionBuilder = new NpgsqlConnectionStringBuilder();
connectionBuilder.Database = dbName;
connectionBuilder.Host = _configFileProvider.PostgresHost;
connectionBuilder.Username = _configFileProvider.PostgresUser;
connectionBuilder.Password = _configFileProvider.PostgresPassword;
connectionBuilder.Port = _configFileProvider.PostgresPort;
connectionBuilder.Enlist = false;
return connectionBuilder.ConnectionString;
}
} }
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Data; using System.Data;
using System.Text.RegularExpressions;
using Dapper; using Dapper;
using NLog; using NLog;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@@ -12,7 +11,6 @@ namespace NzbDrone.Core.Datastore
IDbConnection OpenConnection(); IDbConnection OpenConnection();
Version Version { get; } Version Version { get; }
int Migration { get; } int Migration { get; }
DatabaseType DatabaseType { get; }
void Vacuum(); void Vacuum();
} }
@@ -34,44 +32,13 @@ namespace NzbDrone.Core.Datastore
return _datamapperFactory(); return _datamapperFactory();
} }
public DatabaseType DatabaseType
{
get
{
using (var db = _datamapperFactory())
{
if (db.ConnectionString.Contains(".db"))
{
return DatabaseType.SQLite;
}
else
{
return DatabaseType.PostgreSQL;
}
}
}
}
public Version Version public Version Version
{ {
get get
{ {
using (var db = _datamapperFactory()) using (var db = _datamapperFactory())
{ {
string version; var version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
try
{
version = db.QueryFirstOrDefault<string>("SHOW server_version");
//Postgres can return extra info about operating system on version call, ignore this
version = Regex.Replace(version, @"\(.*?\)", "");
}
catch
{
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
}
return new Version(version); return new Version(version);
} }
} }
@@ -83,7 +50,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var db = _datamapperFactory()) using (var db = _datamapperFactory())
{ {
return db.QueryFirstOrDefault<int>("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1"); return db.QueryFirstOrDefault<int>("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1");
} }
} }
} }
@@ -106,10 +73,4 @@ namespace NzbDrone.Core.Datastore
} }
} }
} }
public enum DatabaseType
{
SQLite,
PostgreSQL
}
} }

View File

@@ -1,8 +1,6 @@
using System; using System;
using System.Data.Common;
using System.Data.SQLite; using System.Data.SQLite;
using NLog; using NLog;
using Npgsql;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
@@ -94,19 +92,10 @@ namespace NzbDrone.Core.Datastore
var db = new Database(migrationContext.MigrationType.ToString(), () => var db = new Database(migrationContext.MigrationType.ToString(), () =>
{ {
DbConnection conn; var conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
if (connectionString.Contains(".db"))
{
conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
}
else
{
conn = new NpgsqlConnection(connectionString);
}
conn.Open(); conn.Open();
return conn; return conn;
}); });

View File

@@ -20,12 +20,12 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder Select(this SqlBuilder builder, params Type[] types) public static SqlBuilder Select(this SqlBuilder builder, params Type[] types)
{ {
return builder.Select(types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", ")); return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", "));
} }
public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types) public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types)
{ {
return builder.Select("DISTINCT " + types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", ")); return builder.Select("DISTINCT " + types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", "));
} }
public static SqlBuilder SelectCount(this SqlBuilder builder) public static SqlBuilder SelectCount(this SqlBuilder builder)
@@ -42,48 +42,41 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter) public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{ {
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence); var wb = new WhereBuilder(filter, true, builder.Sequence);
return builder.Where(wb.ToString(), wb.Parameters);
}
public static SqlBuilder WherePostgres<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{
var wb = new WhereBuilderPostgres(filter, true, builder.Sequence);
return builder.Where(wb.ToString(), wb.Parameters); return builder.Where(wb.ToString(), wb.Parameters);
} }
public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter) public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{ {
var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence); var wb = new WhereBuilder(filter, true, builder.Sequence);
return builder.OrWhere(wb.ToString(), wb.Parameters); return builder.OrWhere(wb.ToString(), wb.Parameters);
} }
public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
{ {
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); var wb = new WhereBuilder(filter, false, builder.Sequence);
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
return builder.Join($"\"{rightTable}\" ON {wb.ToString()}"); return builder.Join($"{rightTable} ON {wb.ToString()}");
} }
public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter)
{ {
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); var wb = new WhereBuilder(filter, false, builder.Sequence);
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
return builder.LeftJoin($"\"{rightTable}\" ON {wb.ToString()}"); return builder.LeftJoin($"{rightTable} ON {wb.ToString()}");
} }
public static SqlBuilder GroupBy<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property) public static SqlBuilder GroupBy<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property)
{ {
var table = TableMapping.Mapper.TableNameMapping(typeof(TModel)); var table = TableMapping.Mapper.TableNameMapping(typeof(TModel));
var propName = property.GetMemberName().Name; var propName = property.GetMemberName().Name;
return builder.GroupBy($"\"{table}\".\"{propName}\""); return builder.GroupBy($"{table}.{propName}");
} }
public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type) public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type)
@@ -145,18 +138,6 @@ namespace NzbDrone.Core.Datastore
return sb.ToString(); return sb.ToString();
} }
private static WhereBuilder GetWhereBuilder(DatabaseType databaseType, Expression filter, bool requireConcrete, int seq)
{
if (databaseType == DatabaseType.PostgreSQL)
{
return new WhereBuilderPostgres(filter, requireConcrete, seq);
}
else
{
return new WhereBuilderSqlite(filter, requireConcrete, seq);
}
}
private static Dictionary<string, object> ToDictionary(this DynamicParameters dynamicParams) private static Dictionary<string, object> ToDictionary(this DynamicParameters dynamicParams)
{ {
var argsDictionary = new Dictionary<string, object>(); var argsDictionary = new Dictionary<string, object>();

View File

@@ -10,12 +10,10 @@ namespace NzbDrone.Core.Datastore
public class LogDatabase : ILogDatabase public class LogDatabase : ILogDatabase
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public LogDatabase(IDatabase database) public LogDatabase(IDatabase database)
{ {
_database = database; _database = database;
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
} }
public IDbConnection OpenConnection() public IDbConnection OpenConnection()
@@ -27,8 +25,6 @@ namespace NzbDrone.Core.Datastore
public int Migration => _database.Migration; public int Migration => _database.Migration;
public DatabaseType DatabaseType => _databaseType;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

View File

@@ -10,12 +10,10 @@ namespace NzbDrone.Core.Datastore
public class MainDatabase : IMainDatabase public class MainDatabase : IMainDatabase
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public MainDatabase(IDatabase database) public MainDatabase(IDatabase database)
{ {
_database = database; _database = database;
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
} }
public IDbConnection OpenConnection() public IDbConnection OpenConnection()
@@ -27,8 +25,6 @@ namespace NzbDrone.Core.Datastore
public int Migration => _database.Migration; public int Migration => _database.Migration;
public DatabaseType DatabaseType => _databaseType;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

View File

@@ -37,22 +37,6 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("MetadataProfileId").AsInt32().WithDefaultValue(1) .WithColumn("MetadataProfileId").AsInt32().WithDefaultValue(1)
.WithColumn("AuthorMetadataId").AsInt32().Unique(); .WithColumn("AuthorMetadataId").AsInt32().Unique();
Create.TableForModel("Books")
.WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0)
.WithColumn("ForeignBookId").AsString().Indexed()
.WithColumn("TitleSlug").AsString().Unique()
.WithColumn("Title").AsString()
.WithColumn("ReleaseDate").AsDateTime().Nullable()
.WithColumn("Links").AsString().Nullable()
.WithColumn("Genres").AsString().Nullable()
.WithColumn("Ratings").AsString().Nullable()
.WithColumn("CleanTitle").AsString().Indexed()
.WithColumn("Monitored").AsBoolean()
.WithColumn("AnyEditionOk").AsBoolean()
.WithColumn("LastInfoSync").AsDateTime().Nullable()
.WithColumn("Added").AsDateTime().Nullable()
.WithColumn("AddOptions").AsString().Nullable();
Create.TableForModel("Series") Create.TableForModel("Series")
.WithColumn("ForeignSeriesId").AsString().Unique() .WithColumn("ForeignSeriesId").AsString().Unique()
.WithColumn("Title").AsString() .WithColumn("Title").AsString()
@@ -84,6 +68,22 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("Ratings").AsString().Nullable() .WithColumn("Ratings").AsString().Nullable()
.WithColumn("Aliases").AsString().WithDefaultValue("[]"); .WithColumn("Aliases").AsString().WithDefaultValue("[]");
Create.TableForModel("Books")
.WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0)
.WithColumn("ForeignBookId").AsString().Indexed()
.WithColumn("TitleSlug").AsString().Unique()
.WithColumn("Title").AsString()
.WithColumn("ReleaseDate").AsDateTime().Nullable()
.WithColumn("Links").AsString().Nullable()
.WithColumn("Genres").AsString().Nullable()
.WithColumn("Ratings").AsString().Nullable()
.WithColumn("CleanTitle").AsString().Indexed()
.WithColumn("Monitored").AsBoolean()
.WithColumn("AnyEditionOk").AsBoolean()
.WithColumn("LastInfoSync").AsDateTime().Nullable()
.WithColumn("Added").AsDateTime().Nullable()
.WithColumn("AddOptions").AsString().Nullable();
Create.TableForModel("Editions") Create.TableForModel("Editions")
.WithColumn("BookId").AsInt32().WithDefaultValue(0) .WithColumn("BookId").AsInt32().WithDefaultValue(0)
.WithColumn("ForeignEditionId").AsString().Unique() .WithColumn("ForeignEditionId").AsString().Unique()
@@ -136,12 +136,12 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("OnUpgrade").AsBoolean().Nullable() .WithColumn("OnUpgrade").AsBoolean().Nullable()
.WithColumn("Tags").AsString().Nullable() .WithColumn("Tags").AsString().Nullable()
.WithColumn("OnRename").AsBoolean().NotNullable() .WithColumn("OnRename").AsBoolean().NotNullable()
.WithColumn("OnReleaseImport").AsBoolean().WithDefaultValue(false) .WithColumn("OnReleaseImport").AsBoolean().WithDefaultValue(0)
.WithColumn("OnHealthIssue").AsBoolean().WithDefaultValue(false) .WithColumn("OnHealthIssue").AsBoolean().WithDefaultValue(0)
.WithColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(false) .WithColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(0)
.WithColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(false) .WithColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(0)
.WithColumn("OnImportFailure").AsBoolean().WithDefaultValue(false) .WithColumn("OnImportFailure").AsBoolean().WithDefaultValue(0)
.WithColumn("OnTrackRetag").AsBoolean().WithDefaultValue(false); .WithColumn("OnTrackRetag").AsBoolean().WithDefaultValue(0);
Create.TableForModel("ScheduledTasks") Create.TableForModel("ScheduledTasks")
.WithColumn("TypeName").AsString().Unique() .WithColumn("TypeName").AsString().Unique()
@@ -327,8 +327,8 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("Label").AsString().NotNullable() .WithColumn("Label").AsString().NotNullable()
.WithColumn("Filters").AsString().NotNullable(); .WithColumn("Filters").AsString().NotNullable();
IfDatabase("sqlite").Create.Index().OnTable("Books").OnColumn("AuthorId"); Create.Index().OnTable("Books").OnColumn("AuthorId");
IfDatabase("sqlite").Create.Index().OnTable("Books").OnColumn("AuthorId").Ascending() Create.Index().OnTable("Books").OnColumn("AuthorId").Ascending()
.OnColumn("ReleaseDate").Ascending(); .OnColumn("ReleaseDate").Ascending();
Delete.Index().OnTable("History").OnColumn("BookId"); Delete.Index().OnTable("History").OnColumn("BookId");
@@ -340,15 +340,12 @@ namespace NzbDrone.Core.Datastore.Migration
.OnColumn("Date").Descending(); .OnColumn("Date").Descending();
Create.Index().OnTable("Authors").OnColumn("Monitored").Ascending(); Create.Index().OnTable("Authors").OnColumn("Monitored").Ascending();
Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending(); Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending();
Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending()
.OnColumn("ReleaseDate").Ascending();
Insert.IntoTable("DelayProfiles").Row(new Insert.IntoTable("DelayProfiles").Row(new
{ {
EnableUsenet = true, EnableUsenet = 1,
EnableTorrent = true, EnableTorrent = 1,
PreferredProtocol = 1, PreferredProtocol = 1,
UsenetDelay = 0, UsenetDelay = 0,
TorrentDelay = 0, TorrentDelay = 0,

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("ImportLists").AddColumn("ShouldSearch").AsBoolean().WithDefaultValue(true); Alter.Table("ImportLists").AddColumn("ShouldSearch").AsInt32().WithDefaultValue(1);
} }
} }
} }

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("DELETE FROM \"Config\" WHERE \"Key\" IN ('folderchmod', 'chownuser')"); Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')");
Execute.WithConnection(ConvertFileChmodToFolderChmod); Execute.WithConnection(ConvertFileChmodToFolderChmod);
} }
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (IDbCommand getFileChmodCmd = conn.CreateCommand()) using (IDbCommand getFileChmodCmd = conn.CreateCommand())
{ {
getFileChmodCmd.Transaction = tran; getFileChmodCmd.Transaction = tran;
getFileChmodCmd.CommandText = @"SELECT ""Value"" FROM ""Config"" WHERE ""Key"" = 'filechmod'"; getFileChmodCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'filechmod'";
var fileChmod = getFileChmodCmd.ExecuteScalar() as string; var fileChmod = getFileChmodCmd.ExecuteScalar() as string;
if (fileChmod != null) if (fileChmod != null)
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (IDbCommand insertCmd = conn.CreateCommand()) using (IDbCommand insertCmd = conn.CreateCommand())
{ {
insertCmd.Transaction = tran; insertCmd.Transaction = tran;
insertCmd.CommandText = "INSERT INTO \"Config\" (\"Key\", \"Value\") VALUES ('chmodfolder', ?)"; insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES ('chmodfolder', ?)";
insertCmd.AddParameter(folderChmod); insertCmd.AddParameter(folderChmod);
insertCmd.ExecuteNonQuery(); insertCmd.ExecuteNonQuery();
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (IDbCommand deleteCmd = conn.CreateCommand()) using (IDbCommand deleteCmd = conn.CreateCommand())
{ {
deleteCmd.Transaction = tran; deleteCmd.Transaction = tran;
deleteCmd.CommandText = "DELETE FROM \"Config\" WHERE \"Key\" = 'filechmod'"; deleteCmd.CommandText = "DELETE FROM Config WHERE Key = 'filechmod'";
deleteCmd.ExecuteNonQuery(); deleteCmd.ExecuteNonQuery();
} }

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("UPDATE \"Notifications\" SET \"Implementation\" = Replace(\"Implementation\", 'DiscordNotifier', 'Notifiarr'),\"ConfigContract\" = Replace(\"ConfigContract\", 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE \"Implementation\" = 'DiscordNotifier';"); Execute.Sql("UPDATE Notifications SET Implementation = Replace(Implementation, 'DiscordNotifier', 'Notifiarr'),ConfigContract = Replace(ConfigContract, 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE Implementation = 'DiscordNotifier';");
} }
} }
} }

View File

@@ -21,14 +21,14 @@ namespace NzbDrone.Core.Datastore.Migration
private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran) private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran)
{ {
var rows = conn.Query<AuthorName>("SELECT \"AuthorMetadata\".\"Id\", \"AuthorMetadata\".\"Name\" FROM \"AuthorMetadata\"", transaction: tran); var rows = conn.Query<AuthorName>("SELECT AuthorMetadata.Id, AuthorMetadata.Name FROM AuthorMetadata", transaction: tran);
foreach (var row in rows) foreach (var row in rows)
{ {
row.SortName = row.Name.ToLastFirst().ToLower(); row.SortName = row.Name.ToLastFirst().ToLower();
} }
var sql = "UPDATE \"AuthorMetadata\" SET \"SortName\" = @SortName WHERE \"Id\" = @Id"; var sql = "UPDATE AuthorMetadata SET SortName = @SortName WHERE Id = @Id";
conn.Execute(sql, rows, transaction: tran); conn.Execute(sql, rows, transaction: tran);
} }

View File

@@ -54,13 +54,13 @@ namespace NzbDrone.Core.Datastore.Migration
_connection = conn; _connection = conn;
_transaction = tran; _transaction = tran;
_profiles = _connection.Query<Profile10>(@"SELECT ""Id"", ""Name"", ""Cutoff"", ""Items"" FROM ""QualityProfiles""", _profiles = _connection.Query<Profile10>(@"SELECT Id, Name, Cutoff, Items FROM QualityProfiles",
transaction: _transaction).ToList(); transaction: _transaction).ToList();
} }
public void Commit() public void Commit()
{ {
var sql = "UPDATE \"QualityProfiles\" SET \"Name\" = @Name, \"Cutoff\" = @Cutoff, \"Items\" = @Items WHERE \"Id\" = @Id"; var sql = "UPDATE QualityProfiles SET Name = @Name, Cutoff = @Cutoff, Items = @Items WHERE Id = @Id";
_connection.Execute(sql, _changedProfiles, transaction: _transaction); _connection.Execute(sql, _changedProfiles, transaction: _transaction);
_changedProfiles.Clear(); _changedProfiles.Clear();

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("UPDATE \"NamingConfig\" SET \"StandardBookFormat\" = \"StandardBookFormat\" || '{ (PartNumber)}'"); Execute.Sql("UPDATE NamingConfig SET StandardBookFormat = StandardBookFormat || '{ (PartNumber)}'");
} }
} }
} }

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Datastore.Migration
private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran) private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran)
{ {
var rows = conn.Query<AuthorName>("SELECT \"AuthorMetadata\".\"Id\", \"AuthorMetadata\".\"Name\" FROM \"AuthorMetadata\"", transaction: tran); var rows = conn.Query<AuthorName>("SELECT AuthorMetadata.Id, AuthorMetadata.Name FROM AuthorMetadata", transaction: tran);
foreach (var row in rows) foreach (var row in rows)
{ {
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Datastore.Migration
row.SortNameLastFirst = row.Name.ToLastFirst().ToLower(); row.SortNameLastFirst = row.Name.ToLastFirst().ToLower();
} }
var sql = "UPDATE \"AuthorMetadata\" SET \"NameLastFirst\" = @NameLastFirst, \"SortName\" = @SortName, \"SortNameLastFirst\" = @SortNameLastFirst WHERE \"Id\" = @Id"; var sql = "UPDATE AuthorMetadata SET NameLastFirst = @NameLastFirst, SortName = @SortName, SortNameLastFirst = @SortNameLastFirst WHERE Id = @Id";
conn.Execute(sql, rows, transaction: tran); conn.Execute(sql, rows, transaction: tran);
} }

View File

@@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
IfDatabase("sqlite").Delete.Index().OnTable("Books").OnColumn("AuthorId"); Delete.Index().OnTable("Books").OnColumn("AuthorId");
IfDatabase("sqlite").Delete.Index().OnTable("Books").OnColumns("AuthorId", "ReleaseDate"); Delete.Index().OnTable("Books").OnColumns("AuthorId", "ReleaseDate");
Create.Index().OnTable("Editions").OnColumn("BookId"); Create.Index().OnTable("Editions").OnColumn("BookId");
} }

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsBoolean().WithDefaultValue(false); Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsInt32().WithDefaultValue(0);
} }
} }
} }

View File

@@ -28,7 +28,9 @@ namespace NzbDrone.Core.Datastore.Migration
Create.Index().OnTable("DownloadHistory").OnColumn("AuthorId"); Create.Index().OnTable("DownloadHistory").OnColumn("AuthorId");
Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId"); Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId");
IfDatabase("sqlite").Execute.WithConnection(InitialImportedDownloadHistory); Execute.WithConnection(InitialImportedDownloadHistory);
Execute.Sql("DELETE From History where EventType = 8;");
} }
private static readonly Dictionary<int, int> EventTypeMap = new Dictionary<int, int>() private static readonly Dictionary<int, int> EventTypeMap = new Dictionary<int, int>()
@@ -54,7 +56,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var cmd = conn.CreateCommand()) using (var cmd = conn.CreateCommand())
{ {
cmd.Transaction = tran; cmd.Transaction = tran;
cmd.CommandText = "SELECT \"AuthorId\", \"DownloadId\", \"EventType\", \"SourceTitle\", \"Date\", \"Data\" FROM \"History\" WHERE \"DownloadId\" IS NOT NULL AND \"EventType\" IN (1, 8, 4, 10, 7) GROUP BY \"EventType\", \"DownloadId\""; cmd.CommandText = "SELECT AuthorId, DownloadId, EventType, SourceTitle, Date, Data FROM History WHERE DownloadId IS NOT NULL AND EventType IN (1, 8, 4, 10, 7) GROUP BY EventType, DownloadId";
using (var reader = cmd.ExecuteReader()) using (var reader = cmd.ExecuteReader())
{ {
@@ -85,15 +87,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var updateCmd = conn.CreateCommand()) using (var updateCmd = conn.CreateCommand())
{ {
updateCmd.Transaction = tran; updateCmd.Transaction = tran;
if (conn.GetType().FullName == "Npgsql.NpgsqlConnection") updateCmd.CommandText = @"INSERT INTO DownloadHistory (EventType, AuthorId, DownloadId, SourceTitle, Date, Protocol, Data) VALUES (?, ?, ?, ?, ?, ?, ?)";
{
updateCmd.CommandText = @"INSERT INTO ""DownloadHistory"" (""EventType"", ""AuthorId"", ""DownloadId"", ""SourceTitle"", ""Date"", ""Protocol"", ""Data"") VALUES ($1, $2, $3, $4, $5, $6, $7)";
}
else
{
updateCmd.CommandText = @"INSERT INTO ""DownloadHistory"" (""EventType"", ""AuthorId"", ""DownloadId"", ""SourceTitle"", ""Date"", ""Protocol"", ""Data"") VALUES (?, ?, ?, ?, ?, ?, ?)";
}
updateCmd.AddParameter(downloadHistoryEventType); updateCmd.AddParameter(downloadHistoryEventType);
updateCmd.AddParameter(seriesId); updateCmd.AddParameter(seriesId);
updateCmd.AddParameter(downloadId); updateCmd.AddParameter(downloadId);

View File

@@ -8,10 +8,10 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("Notifications").AddColumn("OnAuthorDelete").AsBoolean().WithDefaultValue(false); Alter.Table("Notifications").AddColumn("OnAuthorDelete").AsBoolean().WithDefaultValue(0);
Alter.Table("Notifications").AddColumn("OnBookDelete").AsBoolean().WithDefaultValue(false); Alter.Table("Notifications").AddColumn("OnBookDelete").AsBoolean().WithDefaultValue(0);
Alter.Table("Notifications").AddColumn("OnBookFileDelete").AsBoolean().WithDefaultValue(false); Alter.Table("Notifications").AddColumn("OnBookFileDelete").AsBoolean().WithDefaultValue(0);
Alter.Table("Notifications").AddColumn("OnBookFileDeleteForUpgrade").AsBoolean().WithDefaultValue(false); Alter.Table("Notifications").AddColumn("OnBookFileDeleteForUpgrade").AsBoolean().WithDefaultValue(0);
} }
} }
} }

View File

@@ -1,14 +0,0 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(22)]
public class EditionMonitoredIndex : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.Index().OnTable("Editions").OnColumn("Monitored");
}
}
}

View File

@@ -1,58 +0,0 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(23)]
public class postgres_update_timestamp_columns_to_with_timezone : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Authors").AlterColumn("LastInfoSync").AsDateTimeOffset().Nullable();
Alter.Table("Authors").AlterColumn("Added").AsDateTimeOffset().Nullable();
Alter.Table("AuthorMetadata").AlterColumn("Born").AsDateTimeOffset().Nullable();
Alter.Table("AuthorMetadata").AlterColumn("Died").AsDateTimeOffset().Nullable();
Alter.Table("Blocklist").AlterColumn("Date").AsDateTimeOffset().NotNullable();
Alter.Table("Blocklist").AlterColumn("PublishedDate").AsDateTimeOffset().Nullable();
Alter.Table("Books").AlterColumn("ReleaseDate").AsDateTimeOffset().Nullable();
Alter.Table("Books").AlterColumn("LastInfoSync").AsDateTimeOffset().Nullable();
Alter.Table("Books").AlterColumn("Added").AsDateTimeOffset().Nullable();
Alter.Table("BookFiles").AlterColumn("DateAdded").AsDateTimeOffset().Nullable();
Alter.Table("BookFiles").AlterColumn("Modified").AsDateTimeOffset().Nullable();
Alter.Table("Commands").AlterColumn("QueuedAt").AsDateTimeOffset().NotNullable();
Alter.Table("Commands").AlterColumn("StartedAt").AsDateTimeOffset().Nullable();
Alter.Table("Commands").AlterColumn("EndedAt").AsDateTimeOffset().Nullable();
Alter.Table("DownloadClientStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
Alter.Table("DownloadClientStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
Alter.Table("DownloadClientStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
Alter.Table("Editions").AlterColumn("ReleaseDate").AsDateTimeOffset().Nullable();
Alter.Table("ExtraFiles").AlterColumn("Added").AsDateTimeOffset().NotNullable();
Alter.Table("ExtraFiles").AlterColumn("LastUpdated").AsDateTimeOffset().NotNullable();
Alter.Table("History").AlterColumn("Date").AsDateTimeOffset().NotNullable();
Alter.Table("ImportListStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
Alter.Table("ImportListStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
Alter.Table("ImportListStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
Alter.Table("IndexerStatus").AlterColumn("InitialFailure").AsDateTimeOffset().Nullable();
Alter.Table("IndexerStatus").AlterColumn("MostRecentFailure").AsDateTimeOffset().Nullable();
Alter.Table("IndexerStatus").AlterColumn("DisabledTill").AsDateTimeOffset().Nullable();
Alter.Table("MetadataFiles").AlterColumn("LastUpdated").AsDateTimeOffset().NotNullable();
Alter.Table("MetadataFiles").AlterColumn("Added").AsDateTimeOffset().Nullable();
Alter.Table("PendingReleases").AlterColumn("Added").AsDateTimeOffset().NotNullable();
Alter.Table("ScheduledTasks").AlterColumn("LastExecution").AsDateTimeOffset().NotNullable();
Alter.Table("ScheduledTasks").AlterColumn("LastStartTime").AsDateTimeOffset().Nullable();
Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable();
}
protected override void LogDbUpgrade()
{
Alter.Table("Logs").AlterColumn("Time").AsDateTimeOffset().NotNullable();
Alter.Table("VersionInfo").AlterColumn("AppliedOn").AsDateTimeOffset().Nullable();
}
protected override void CacheDbUpgrade()
{
Alter.Table("HttpResponse").AlterColumn("LastRefresh").AsDateTimeOffset().Nullable();
Alter.Table("HttpResponse").AlterColumn("Expiry").AsDateTimeOffset().Nullable();
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using FluentMigrator.Runner; using FluentMigrator.Runner;
using FluentMigrator.Runner.Generators;
using FluentMigrator.Runner.Initialization; using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.Processors; using FluentMigrator.Runner.Processors;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -35,16 +34,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_logger.Info("*** Migrating {0} ***", connectionString); _logger.Info("*** Migrating {0} ***", connectionString);
ServiceProvider serviceProvider; var serviceProvider = new ServiceCollection()
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog()) .AddLogging(b => b.AddNLog())
.AddFluentMigratorCore() .AddFluentMigratorCore()
.ConfigureRunner( .ConfigureRunner(
builder => builder builder => builder
.AddPostgres()
.AddNzbDroneSQLite() .AddNzbDroneSQLite()
.WithGlobalConnectionString(connectionString) .WithGlobalConnectionString(connectionString)
.WithMigrationsIn(Assembly.GetExecutingAssembly())) .WithMigrationsIn(Assembly.GetExecutingAssembly()))
@@ -54,14 +48,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
opt.PreviewOnly = false; opt.PreviewOnly = false;
opt.Timeout = TimeSpan.FromSeconds(60); opt.Timeout = TimeSpan.FromSeconds(60);
}) })
.Configure<SelectingProcessorAccessorOptions>(cfg =>
{
cfg.ProcessorId = db;
})
.Configure<SelectingGeneratorAccessorOptions>(cfg =>
{
cfg.GeneratorId = db;
})
.BuildServiceProvider(); .BuildServiceProvider();
using (var scope = serviceProvider.CreateScope()) using (var scope = serviceProvider.CreateScope())

View File

@@ -20,9 +20,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ILogger<NzbDroneSQLiteProcessor> logger, ILogger<NzbDroneSQLiteProcessor> logger,
IOptionsSnapshot<ProcessorOptions> options, IOptionsSnapshot<ProcessorOptions> options,
IConnectionStringAccessor connectionStringAccessor, IConnectionStringAccessor connectionStringAccessor,
IServiceProvider serviceProvider, IServiceProvider serviceProvider)
SQLiteQuoter quoter) : base(factory, generator, logger, options, connectionStringAccessor, serviceProvider)
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
{ {
} }

View File

@@ -1,27 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace NzbDrone.Core.Datastore
{
public class PostgresOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string User { get; set; }
public string Password { get; set; }
public string MainDb { get; set; }
public string LogDb { get; set; }
public string CacheDb { get; set; }
public static PostgresOptions GetOptions()
{
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var postgresOptions = new PostgresOptions();
config.GetSection("Readarr:Postgres").Bind(postgresOptions);
return postgresOptions;
}
}
}

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