Compare commits

..

3 Commits

Author SHA1 Message Date
bakerboy448
e0b7a78568 CHANGELOG-v0.1.1.1320 2022-05-10 19:31:21 -05:00
bakerboy448
bc31f10770 changelog script workaround bad tags 2022-05-10 19:30:20 -05:00
bakerboy448
98ca518178 create changelog script 2022-05-10 19:29:58 -05:00
284 changed files with 2303 additions and 5645 deletions

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing open and closed issues
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
@@ -42,14 +42,12 @@ body:
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: |
- OS:
- Readarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
- Database:
render: markdown
validations:
required: true

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing open and closed issues
- label: I have searched the existing issues
required: true
- type: textarea
attributes:

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@readarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,8 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.

View File

@@ -15,7 +15,7 @@ variables:
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.302'
dotnetVersion: '6.0.201'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
@@ -66,13 +66,16 @@ stages:
inputs:
version: $(dotnetVersion)
- bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi
displayName: Extra Platform Support
displayName: Enable FreeBSD Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
@@ -84,27 +87,29 @@ stages:
NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output
- publish: $(outputFolder)
artifact: '$(osName)Backend'
- task: PublishPipelineArtifact@1
inputs:
path: $(outputFolder)
artifact: '$(osName)Backend'
artifactType: 'pipeline'
parallel: true
parallelCount: 100
displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
artifact: MacCoreTests
displayName: Publish MacOS Test Package
- stage: Build_Backend_Other
displayName: Build Backend (Other OS)
@@ -136,29 +141,25 @@ stages:
inputs:
version: $(dotnetVersion)
- bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi
displayName: Extra Platform Support
displayName: Enable FreeBSD Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-extra-platforms
- bash: ./build.sh --backend --enable-bsd
displayName: Build Readarr Backend
env:
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
displayName: Frontend
@@ -261,35 +262,35 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-extra-platforms
- bash: ./build.sh --packages --enable-bsd
displayName: Create Packages
- bash: |
find . -name "Readarr" -exec chmod a+x {} \;
find . -name "Readarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits
- task: ArchiveFiles@2
displayName: Create win-x64 zip
displayName: Create Windows Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2
displayName: Create win-x86 zip
displayName: Create Windows x86 Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2
displayName: Create osx-x64 app
displayName: Create MacOS x64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-x64 tar
displayName: Create MacOS x64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
@@ -297,14 +298,14 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 app
displayName: Create MacOS arm64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
displayName: Create MacOS arm64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
@@ -312,7 +313,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
displayName: Create Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar'
@@ -320,7 +321,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar
displayName: Create Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar'
@@ -328,15 +329,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x86 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
displayName: Create ARM32 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar'
@@ -344,7 +337,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm tar
displayName: Create ARM32 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
@@ -352,7 +345,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
displayName: Create Linux arm64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar'
@@ -360,7 +353,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar
displayName: Create ARM64 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar'
@@ -368,7 +361,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create freebsd-x64 tar
displayName: Create FreeBSD Core Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar'
@@ -420,22 +413,22 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'osx-x64'
testName: 'MacCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }}
WindowsCore:
osName: 'Windows'
testName: 'win-x64'
testName: 'WindowsCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }}
LinuxCore:
osName: 'Linux'
testName: 'linux-x64'
testName: 'LinuxCore'
poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }}
FreebsdCore:
osName: 'Linux'
testName: 'freebsd-x64'
testName: 'FreebsdCore'
poolName: 'FreeBSD'
imageName:
@@ -454,7 +447,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
@@ -482,12 +475,8 @@ stages:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests
artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: ${{ variables.linuxImage }}
@@ -498,15 +487,9 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
displayName: 'Install .net core'
inputs:
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
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -529,57 +512,6 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
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
displayName: Integration
@@ -591,17 +523,17 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'osx-x64'
testName: 'MacCore'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
testName: 'win-x64'
testName: 'WindowsCore'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip'
LinuxCore:
osName: 'Linux'
testName: 'linux-x64'
testName: 'LinuxCore'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz'
@@ -618,7 +550,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -648,66 +580,6 @@ stages:
failTaskOnFailedTests: true
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
displayName: Integration Native FreeBSD
workspace:
@@ -723,14 +595,14 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'freebsd-x64-tests'
artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
itemPattern: '/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -757,15 +629,11 @@ stages:
strategy:
matrix:
alpine:
testName: 'linux-musl-x64'
artifactName: linux-musl-x64-tests
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine
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:
vmImage: ${{ variables.linuxImage }}
@@ -775,15 +643,9 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
displayName: 'Install .net core'
inputs:
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
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -829,17 +691,14 @@ stages:
matrix:
Linux:
osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Mac:
osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz'
Windows:
osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip'
@@ -856,7 +715,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(artifactName)-tests'
artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -1050,4 +909,3 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)

View File

@@ -27,22 +27,15 @@ UpdateVersionNumber()
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')
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
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
}
#todo enable sdk with
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
EnableExtraPlatforms()
{
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
}
@@ -299,8 +292,7 @@ if [ $# -eq 0 ]; then
PACKAGES=YES
INSTALLER=NO
LINT=YES
ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
ENABLE_BSD=NO
fi
while [[ $# -gt 0 ]]
@@ -312,12 +304,8 @@ case $key in
BACKEND=YES
shift # past argument
;;
--enable-bsd|--enable-extra-platforms)
ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
--enable-bsd)
ENABLE_BSD=YES
shift # past argument
;;
-r|--runtime)
@@ -361,17 +349,12 @@ esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ];
then
UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
EnableExtraPlatforms
EnableBsdSupport
fi
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -381,10 +364,9 @@ then
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -423,10 +405,9 @@ then
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
if [ "$ENABLE_BSD" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi
else
Package "$FRAMEWORK" "$RID"

View File

@@ -0,0 +1,94 @@
# New Beta Release
Readarr v0.1.1.1320 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.
# Announcements
- Automated API Documentation Updates recently implemented
- [Wiki Contributions](https://wiki.servarr.com/readarr) and updates welcome and encouraged on the Wiki itself or via GitHub
# Additional Commentary
- [Lidarr v1 released](https://www.reddit.com/r/Lidarr/comments/ul0b2w/new_release_develop_v1012578/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- Radarr Postgres Database Support in `nightly` and `develop`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
# Releases
## Native
- [GitHub Releases](https://github.com/Readarr/Readarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/readarr/installation)
## Docker
- [hotio/Readarr:testing](https://hotio.dev/containers/readarr)
- [lscr.io/linuxserver/Readarr:develop](https://docs.linuxserver.io/images/docker-readarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.1.1.1320 (changes since v0.1.0.1248)
- Fixed: Correct User-Agent api logging
- Fixed: UI hiding search results with duplicate GUIDs
- New: Add date picker for custom filter dates
- Fixed: Interactive Search Filter not filtering multiple qualities in the same filter row
- Fixed: Clarify Qbit Content Path Error
- Fixed: Properly handle 119 error code from Synology Download Station
- Fixed: API error when sending payload without optional parameters
- Fixed: Cleanup Temp files after backup creation
- Fixed: Loading old commands from database
- New: Update Cert Validation Help Text
- Fixed: Clarify Indexer Priority Helptext
- Fixed: Improve help text for download client Category
- Fixed: IPv4 instead of IP4
- New: Add Validations for Recycle Bin Folder
- New: .NET 6.0.3
- Fixed: Healthcheck warning message used incorrect variable
- Fixed: Assume SABnzbd develop version is 3.0.0 if not specified
- Fixed: Updater version number logging
- Fixed: Update from version in logs
- New: Add more information about Windows service to installer
- Fixed: Recycle bin log message
- Fixed: Make authentication cookie name unique to Readarr
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,2 @@
- Automated API Documentation Updates recently implemented
- [Wiki Contributions](https://wiki.servarr.com/readarr) and updates welcome and encouraged on the Wiki itself or via GitHub

View File

@@ -0,0 +1,6 @@
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.

View File

@@ -0,0 +1,6 @@
- **Users who do not wish to be on the alpha `nightly` or beta `develop` testing branches should take advantage of this parity and switch to `master`
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.**
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.**

View File

@@ -0,0 +1,4 @@
- [Lidarr v1 released](https://www.reddit.com/r/Lidarr/comments/ul0b2w/new_release_develop_v1012578/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- Radarr Postgres Database Support in `nightly` and `develop`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)

View File

@@ -39,7 +39,6 @@ module.exports = {
plugins: [
'filenames',
'react',
'react-hooks',
'simple-import-sort',
'import'
],
@@ -309,9 +308,7 @@ module.exports = {
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
'react/jsx-wrap-multilines': 2
},
overrides: [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@@ -43,26 +42,10 @@ class BookIndexOverview extends Component {
this.state = {
isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false,
overview: ''
isDeleteAuthorModalOpen: false
};
}
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
@@ -101,6 +84,7 @@ class BookIndexOverview extends Component {
const {
id,
title,
overview,
monitored,
titleSlug,
nextAiring,
@@ -134,7 +118,6 @@ class BookIndexOverview extends Component {
} = statistics;
const {
overview,
isEditAuthorModalOpen,
isDeleteAuthorModalOpen
} = this.state;
@@ -284,6 +267,7 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,

View File

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

View File

@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
import Portal from 'Components/Portal';
import Scroller from 'Components/Scroller/Scroller';
import { icons, scrollDirections, sizes } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';

View File

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

View File

@@ -33,7 +33,7 @@ function ConfirmModal(props) {
return () => unbindShortcut('enter', onConfirm);
}
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
}, [isOpen, onConfirm]);
return (
<Modal

View File

@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import { sizes } from 'Helpers/Props';
import { isIOS } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { setScrollLock } from 'Utilities/scrollLock';
import ModalError from './ModalError';
import styles from './Modal.css';

View File

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

View File

@@ -1,12 +1,23 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
import Scroller from 'Components/Scroller/Scroller';
import { scrollDirections } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
class PageContentBody extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
}
//
// Listeners
@@ -30,8 +41,10 @@ class PageContentBody extends Component {
...otherProps
} = this.props;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
return (
<Scroller
<ScrollerComponent
className={className}
scrollDirection={scrollDirections.VERTICAL}
{...otherProps}
@@ -40,7 +53,7 @@ class PageContentBody extends Component {
<div className={innerClassName}>
{children}
</div>
</Scroller>
</ScrollerComponent>
);
}
}

View File

@@ -1,5 +1,4 @@
.jumpBar {
z-index: $pageJumpBarZIndex;
display: flex;
align-content: stretch;
align-items: stretch;

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { Scrollbars } from 'react-custom-scrollbars';
import { scrollDirections } from 'Helpers/Props';
import styles from './OverlayScroller.css';

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Measure from 'Components/Measure';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import styles from './SwipeHeader.css';
function cursorPosition(event) {

View File

@@ -5,7 +5,7 @@ import { Manager, Popper, Reference } from 'react-popper';
import Portal from 'Components/Portal';
import { kinds, tooltipPositions } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import styles from './Tooltip.css';
let maxWidth = null;

View File

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

View File

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

@@ -148,10 +148,11 @@ class AddNewItem extends Component {
);
} else if (item.book) {
const book = item.book;
const edition = book.editions.find((x) => x.monitored);
return (
<AddNewBookSearchResultConnector
key={item.id}
isExistingBook={'id' in book && book.id !== 0}
isExistingBook={'id' in edition && edition.id !== 0}
isExistingAuthor={'id' in book.author && book.author.id !== 0}
{...book}
/>

View File

@@ -138,20 +138,17 @@ class AddNewBookSearchResult extends Component {
null
}
{
editions && editions.length > 1 ?
<Link
className={styles.mbLink}
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
onPress={this.onTVDBLinkPress}
>
<Icon
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link> : null
}
<Link
className={styles.mbLink}
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
onPress={this.onTVDBLinkPress}
>
<Icon
className={styles.mbLinkIcon}
name={icons.EXTERNAL_LINK}
size={28}
/>
</Link>
</div>
</div>
@@ -221,7 +218,7 @@ AddNewBookSearchResult.propTypes = {
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
author: PropTypes.object,
editions: PropTypes.arrayOf(PropTypes.object),
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingBook: PropTypes.bool.isRequired,
isExistingAuthor: PropTypes.bool.isRequired,

View File

@@ -126,7 +126,7 @@ class AddAuthorOptionsForm extends Component {
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
<FormLabel>
{translate('MetadataProfile')}
Metadata Profile
{
includeNoneMetadataProfile &&
@@ -173,7 +173,7 @@ class AddAuthorOptionsForm extends Component {
AddAuthorOptionsForm.propTypes = {
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
monitorNewItems: PropTypes.object.isRequired,
monitorNewItems: PropTypes.string.isRequired,
qualityProfileId: PropTypes.object,
metadataProfileId: PropTypes.object,
showMetadataProfile: PropTypes.bool.isRequired,

View File

@@ -20,7 +20,6 @@ function HostSettings(props) {
bindAddress,
port,
urlBase,
instanceName,
enableSsl,
sslPort,
sslCertPath,
@@ -58,7 +57,6 @@ function HostSettings(props) {
name="port"
min={1}
max={65535}
autocomplete="off"
helpTextWarning={translate('PortHelpTextWarning')}
onChange={onInputChange}
{...port}
@@ -80,22 +78,6 @@ function HostSettings(props) {
/>
</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
advancedSettings={advancedSettings}
isAdvanced={true}

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ function PendingChangesModal(props) {
useEffect(() => {
bindShortcut('enter', onConfirm);
}, [bindShortcut, onConfirm]);
}, [onConfirm]);
return (
<Modal

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import getProviderState from 'Utilities/State/getProviderState';
import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
@@ -28,39 +25,18 @@ export const defaultState = {
export const FETCH_EDITIONS = 'editions/fetchEditions';
export const CLEAR_EDITIONS = 'editions/clearEditions';
export const SAVE_EDITIONS = 'editions/saveEditions';
//
// Action Creators
export const fetchEditions = createThunk(FETCH_EDITIONS);
export const clearEditions = createAction(CLEAR_EDITIONS);
export const saveEditions = createThunk(SAVE_EDITIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[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
});
})
]));
}
[FETCH_EDITIONS]: createFetchHandler(section, '/edition')
});
//

View File

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

View File

@@ -177,8 +177,7 @@ export const defaultState = {
{
name: 'size',
label: 'Size',
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
type: filterBuilderTypes.NUMBER
},
{
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 hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector';
import createClientSideCollectionSelector from './createClientSideCollectionSelector';
function createUnoptimizedSelector(uiSection) {
return createSelector(
createBooksClientSideCollectionSelector(uiSection),
createClientSideCollectionSelector('books', uiSection),
(books) => {
const items = books.items.map((s) => {
const {

View File

@@ -1,16 +1,18 @@
import { createSelector } from 'reselect';
import createBookAuthorSelector from './createBookAuthorSelector';
import createBookSelector from './createBookSelector';
function createBookQualityProfileSelector() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
createBookAuthorSelector(),
(qualityProfiles, author) => {
if (!author) {
createBookSelector(),
(qualityProfiles, book) => {
if (!book) {
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 { createSelector } from 'reselect';
import filterCollection from 'Utilities/Array/filterCollection';
import sortCollection from 'Utilities/Array/sortCollection';
import createCustomFiltersSelector from './createCustomFiltersSelector';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
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) {
return createSelector(
@@ -12,8 +127,8 @@ function createClientSideCollectionSelector(section, uiSection) {
(sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
const filtered = filterCollection(state.items, state);
const sorted = sortCollection(filtered, state);
const filtered = filter(state.items, state);
const sorted = sort(filtered, state);
return {
...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

@@ -1,7 +1,4 @@
@define-mixin scrollbar {
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 10px;
height: 10px;

View File

@@ -1,5 +1,4 @@
module.exports = {
pageJumpBarZIndex: 10,
modalZIndex: 1000,
popperZIndex: 2000
};

View File

@@ -20,11 +20,10 @@ class About extends Component {
packageVersion,
packageAuthor,
isNetCore,
isMono,
isDocker,
runtimeVersion,
migrationVersion,
databaseVersion,
databaseType,
appData,
startupPath,
mode,
@@ -49,6 +48,14 @@ class About extends Component {
/>
}
{
isMono &&
<DescriptionListItem
title={translate('MonoVersion')}
data={runtimeVersion}
/>
}
{
isNetCore &&
<DescriptionListItem
@@ -70,11 +77,6 @@ class About extends Component {
data={migrationVersion}
/>
<DescriptionListItem
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
/>
<DescriptionListItem
title={translate('AppDataDirectory')}
data={appData}
@@ -112,11 +114,10 @@ About.propTypes = {
packageVersion: PropTypes.string,
packageAuthor: PropTypes.string,
isNetCore: PropTypes.bool.isRequired,
isMono: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired,
databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired,
appData: PropTypes.string.isRequired,
startupPath: 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

@@ -10,7 +10,3 @@ export function isMobile() {
export function isIOS() {
return mobileDetect.is('iOS');
}
export function isFirefox() {
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
}

View File

@@ -33,7 +33,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.7",
"@microsoft/signalr": "6.0.3",
"@sentry/browser": "6.18.2",
"@sentry/integrations": "6.18.2",
"ansi-colors": "4.1.1",
@@ -58,7 +58,7 @@
"react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0",
"react-autosuggest": "10.1.0",
"react-custom-scrollbars-2": "4.5.0",
"react-custom-scrollbars": "4.2.1",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dnd-multi-backend": "6.0.2",
@@ -109,7 +109,6 @@
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.23.4",
"eslint-plugin-react": "7.24.0",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.1.0",
"file-loader": "6.2.0",

View File

@@ -0,0 +1,110 @@
#!/bin/bash
# Generate a Markdown change log of pull requests from commits between two tags
scriptDir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
ghRepo="Readarr"
#branch="develop"
#read -r -p "What Repo?: " ghRepo
#read -r -p "What Org?: [Default:$ghRepo]" ghOrg
read -r -p "What Branch? [master|develop|nightly]:" branch
ghOrg=${ghOrg:-$ghRepo}
ghRepoUrl=https://github.com/$ghOrg/$ghRepo
case "${branch}" in
master)
hotioBranch='release'
lsioBranch='latest'
branchType='Stable'
;;
develop)
hotioBranch='testing'
lsioBranch='develop'
branchType='Beta'
;;
nightly)
hotioBranch='nightly'
lsioBranch='nightly'
branchType='Alpha'
;;
esac
baseDir=$(dirname "$scriptDir")
changelogDir="$baseDir/changelogs/"
templateDir="$changelogDir/templates/"
# Get a list of all tags in reverse order
# Assumes the tags are in version format like v1.2.3
## gitTags=$(git ls-remote -t --exit-code --refs --sort='-v:refname' "$ghRepoUrl" | sed -E 's/^[[:xdigit:]]+[[:space:]]+refs\/tags\/(.+)/\1/g')
# Make the tags an array
# shellcheck disable=SC2206
## tags=($gitTags)
# Prompt for Tags due to bad Tags on GH (Sonarr v2)
# Commented out automation
##latestTag=${tags[0]}
##previousTag=${tags[1]}
read -r -p "Enter Latest Tag:" latestTag
read -r -p "Enter Previous Tag:" previousTag
# Get a log of commits that occurred between two tags
# See Pretty format placeholders at https://git-scm.com/docs/pretty-formats
# -i -E --grep="(Fixed:|New:)"'
commits=$(git log --pretty=format:" - %s%n" -i -E --grep="(Fixed:|New:)" "$previousTag".."$latestTag")
# Store our changelog in a variable to be saved to a file at the end
markdown="# New ${branchType^} Release"
markdown+='\n\n'
markdown+="$ghRepo $latestTag has been released on \`$branch\`"
markdown+='\n\n'
branchmsg=$(cat "$templateDir"/branch-$branch.md)
if [ -n "$branchmsg" ]; then
{
markdown+=$branchmsg
markdown+='\n\n'
}
fi
markdown+="# Announcements"
markdown+='\n\n'
markdown+=$(cat "$templateDir"/announcements.md)
markdown+='\n\n'
markdown+="# Additional Commentary"
markdown+='\n\n'
markdown+=$(cat "$templateDir"/commentary.md)
markdown+='\n\n'
markdown+="# Releases"
markdown+='\n\n'
markdown+="## Native"
markdown+="\n\n"
markdown+="- [GitHub Releases]($ghRepoUrl/releases)"
markdown+="\n\n"
markdown+="- [Wiki Installation Instructions](https://wiki.servarr.com/${ghRepo,,}/installation)"
markdown+="\n\n"
markdown+="## Docker"
markdown+="\n\n"
markdown+="- [hotio/$ghRepo:$hotioBranch](https://hotio.dev/containers/${ghRepo,,})"
markdown+="\n\n"
markdown+="- [lscr.io/linuxserver/$ghRepo:$lsioBranch](https://docs.linuxserver.io/images/docker-${ghRepo,,})"
markdown+="\n\n"
markdown+="## NAS Packages"
markdown+="\n\n"
markdown+="- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally"
markdown+="\n\n"
markdown+="- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally"
markdown+="\n\n"
markdown+="------------"
markdown+="\n\n"
markdown+="# Release Notes"
markdown+="\n\n"
markdown+="## $latestTag (changes since $previousTag)"
markdown+="\n\n"
markdown+="$commits"
markdown+="\n\n"
markdown+=" - Other bug fixes and improvements, see GitHub history"
# Loop over each commit and look for merged pull requests
#for COMMIT in $COMMITS; do
#done
# Save our markdown to a file
mkdir -p "$changelogDir"
echo -e "$markdown" >"$changelogDir/CHANGELOG-$latestTag.md"
exit 0

View File

@@ -4,62 +4,60 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.123" />
<PackageVersion Include="DryIoc.dll" Version="5.2.0" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" />
<PackageVersion Include="DryIoc.dll" Version="4.8.7" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" />
<PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" />
<PackageVersion Include="FluentValidation" Version="8.6.2" />
<PackageVersion Include="Ical.Net" Version="4.2.0" />
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" />
<PackageVersion Include="Mailkit" Version="3.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" 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.Hosting.WindowsServices" 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="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="MonoTorrent" Version="2.0.6" />
<PackageVersion Include="NBuilder" Version="6.1.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
<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="NUnit" Version="3.13.3" />
<PackageVersion Include="NUnit" Version="3.13.2" />
<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" Version="106.15.0" />
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
<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="SixLabors.ImageSharp" Version="2.1.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />
<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.IO.Abstractions.TestingHelpers" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="16.1.20" />
<PackageVersion Include="System.IO.Abstractions" Version="16.1.20" />
<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.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.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.Json" Version="6.0.5" />
<PackageVersion Include="System.Text.Json" Version="6.0.2" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
<PackageVersion Include="Unity" Version="5.11.10" />
</ItemGroup>
</Project>

View File

@@ -1,33 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<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="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="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>
<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>

View File

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

View File

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

View File

@@ -437,6 +437,24 @@ namespace NzbDrone.Common.Test.DiskTests
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_not_copy_casesensitive_folder()
{
MonoOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
// Note: Although technically possible top copy to different case, we're not allowing it
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_ignore_nfs_temp_file()
{
@@ -522,6 +540,26 @@ namespace NzbDrone.Common.Test.DiskTests
source.FullName.GetActualCasing().Should().Be(destination.FullName);
}
[Test]
public void MoveFolder_should_rename_casesensitive_folder()
{
MonoOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
Directory.Exists(source.FullName).Should().Be(false);
Directory.Exists(destination.FullName).Should().Be(true);
}
[Test]
public void should_throw_if_destination_is_readonly()
{

View File

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

View File

@@ -207,7 +207,6 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
@@ -308,6 +307,11 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_follow_redirects_to_https()
{
if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
{
Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
}
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/")
.Build();

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Instrumentation;
@@ -60,8 +60,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[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("/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 ***")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
@@ -72,41 +70,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
// Discord
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
}
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
//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(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
public void should_cleanGoodRead_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);

View File

@@ -279,7 +279,7 @@ namespace NzbDrone.Common.Test
[Test]
public void GetUpdateClientExePath()
{
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update".AsOsAgnostic().ProcessNameToExe());
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update.exe".AsOsAgnostic());
}
[Test]

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
@@ -171,7 +171,7 @@ namespace NzbDrone.Common.Test
var processStarted = new ManualResetEventSlim();
string suffix;
if (OsInfo.IsWindows)
if (OsInfo.IsWindows || PlatformInfo.IsMono)
{
suffix = ".exe";
}

View File

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

View File

@@ -1,7 +1,17 @@
using System;
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace NzbDrone.Common.EnvironmentInfo
{
public enum PlatformType
{
DotNet = 0,
Mono = 1,
NetCore = 2
}
public interface IPlatformInfo
{
Version Version { get; }
@@ -9,18 +19,38 @@ namespace NzbDrone.Common.EnvironmentInfo
public class PlatformInfo : IPlatformInfo
{
private static readonly Regex MonoVersionRegex = new Regex(@"(?<=\W|^)(?<version>\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static PlatformType _platform;
private static Version _version;
static PlatformInfo()
{
_platform = PlatformType.NetCore;
_version = Environment.Version;
}
public static PlatformType Platform => _platform;
public static bool IsMono => Platform == PlatformType.Mono;
public static bool IsDotNet => Platform == PlatformType.DotNet;
public static bool IsNetCore => Platform == PlatformType.NetCore;
public static string PlatformName
{
get
{
return ".NET";
if (IsDotNet)
{
return ".NET";
}
else if (IsMono)
{
return "Mono";
}
else
{
return ".NET Core";
}
}
}
@@ -30,5 +60,107 @@ namespace NzbDrone.Common.EnvironmentInfo
{
return _version;
}
private static Version GetMonoVersion()
{
try
{
var type = Type.GetType("Mono.Runtime");
if (type != null)
{
var displayNameMethod = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
if (displayNameMethod != null)
{
var displayName = displayNameMethod.Invoke(null, null).ToString();
var versionMatch = MonoVersionRegex.Match(displayName);
if (versionMatch.Success)
{
return new Version(versionMatch.Groups["version"].Value);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Couldnt get Mono version: " + ex.ToString());
}
return new Version();
}
private static Version GetDotNetVersion()
{
try
{
const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";
using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey))
{
if (ndpKey == null)
{
return new Version(4, 0);
}
var releaseKey = (int)ndpKey.GetValue("Release");
if (releaseKey >= 528040)
{
return new Version(4, 8, 0);
}
if (releaseKey >= 461808)
{
return new Version(4, 7, 2);
}
if (releaseKey >= 461308)
{
return new Version(4, 7, 1);
}
if (releaseKey >= 460798)
{
return new Version(4, 7);
}
if (releaseKey >= 394802)
{
return new Version(4, 6, 2);
}
if (releaseKey >= 394254)
{
return new Version(4, 6, 1);
}
if (releaseKey >= 393295)
{
return new Version(4, 6);
}
if (releaseKey >= 379893)
{
return new Version(4, 5, 2);
}
if (releaseKey >= 378675)
{
return new Version(4, 5, 1);
}
if (releaseKey >= 378389)
{
return new Version(4, 5);
}
}
}
catch (Exception ex)
{
Console.WriteLine("Couldnt get .NET framework version: " + ex.ToString());
}
return new Version(4, 0);
}
}
}

View File

@@ -240,9 +240,9 @@ namespace NzbDrone.Common.Extensions
return null;
}
public static string ProcessNameToExe(this string processName)
public static string ProcessNameToExe(this string processName, PlatformType runtime)
{
if (OsInfo.IsWindows)
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
{
processName += ".exe";
}
@@ -250,6 +250,11 @@ namespace NzbDrone.Common.Extensions
return processName;
}
public static string ProcessNameToExe(this string processName)
{
return processName.ProcessNameToExe(PlatformInfo.Platform);
}
public static string GetLongestCommonPath(this List<string> paths)
{
var firstPath = paths.First();
@@ -342,9 +347,9 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
}
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
{
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
}
public static string GetDatabase(this IAppFolderInfo appFolderInfo)

View File

@@ -55,8 +55,7 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.PermanentRedirect;
StatusCode == HttpStatusCode.SeeOther;
public string[] GetCookieHeaders()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
@@ -11,14 +11,13 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// 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(@"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(@"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(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// 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"),
@@ -47,14 +46,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Good Reads
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),
// Discord
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", 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);

View File

@@ -38,6 +38,16 @@ namespace NzbDrone.Common.Instrumentation
return;
}
if (PlatformInfo.IsMono)
{
if ((exception is TypeInitializationException && exception.InnerException is DllNotFoundException) ||
exception is DllNotFoundException)
{
Logger.Debug(exception, "Minor Fail: " + exception.Message);
return;
}
}
Console.WriteLine("EPIC FAIL: {0}", exception);
Logger.Fatal(exception, "EPIC FAIL.");
}

View File

@@ -109,6 +109,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Debug = false;
o.DiagnosticLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release;
if (PlatformInfo.IsMono)
{
// Mono 6.0 broke GzipStream.WriteAsync
// TODO: Check specific version
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
}
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.Environment = BuildInfo.Branch;
@@ -151,6 +158,14 @@ namespace NzbDrone.Common.Instrumentation.Sentry
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
if (osInfo.Name != null && PlatformInfo.IsMono)
{
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
scope.Contexts.OperatingSystem.RawDescription = osInfo.FullName;
scope.Contexts.OperatingSystem.Version = osInfo.Version.ToString();
}
});
}

View File

@@ -127,18 +127,7 @@ namespace NzbDrone.Common.Processes
try
{
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
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);
}
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
}
catch (Exception e)
{
@@ -377,6 +366,11 @@ namespace NzbDrone.Common.Processes
private (string Path, string Args) GetPathAndArgs(string path, string args)
{
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
return ("mono", $"--debug {path} {args}");
}
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
{
return ("cmd.exe", $"/c {path} {args}");

View File

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

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_author_for_trackfile()
{
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);
foreach (var track in tracks)
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_trackfile_if_not_joined()
{
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)
{
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Datastore
{
var db = Mocker.Resolve<IDatabase>();
var files = MediaFileRepository.Query(db,
new SqlBuilder(db.DatabaseType)
new SqlBuilder()
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
.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
{
[TestFixture]
public class WhereBuilderSqliteFixture : CoreTest
public class WhereBuilderFixture : CoreTest
{
private WhereBuilderSqlite _subject;
private WhereBuilder _subject;
[OneTimeSetUp]
public void MapTables()
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
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]
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
{
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());
}
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
{
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\")");
}

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.Data.SQLite;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework
{
@@ -53,7 +47,6 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest
{
private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -72,7 +65,8 @@ namespace NzbDrone.Core.Test.Framework
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
{
var database = CreateDatabase(migrationContext);
var factory = Mocker.Resolve<DbFactory>();
var database = factory.Create(migrationContext);
Mocker.SetConstant(database);
switch (MigrationType)
@@ -104,65 +98,6 @@ namespace NzbDrone.Core.Test.Framework
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()
{
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
@@ -173,13 +108,6 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath();
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<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)
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null)
{
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 NzbDrone.Common.Disk;
using NzbDrone.Test.Common.AutoMoq;
using Unity.Resolution;
namespace NzbDrone.Core.Test.Framework
{
@@ -15,9 +14,12 @@ namespace NzbDrone.Core.Test.Framework
[SetUp]
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();
IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
}
public class TestDatabase : ITestDatabase
@@ -31,8 +30,6 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection)
{
_eventAggregator = new Mock<IEventAggregator>().Object;

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Setup(s => s.GetAuthor(It.IsAny<int>()))
.Returns(_author);
Mocker.GetMock<ISearchForReleases>()
Mocker.GetMock<ISearchForNzb>()
.Setup(s => s.AuthorSearch(_author.Id, false, true, false))
.Returns(new List<DownloadDecision>());
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Subject.Execute(new AuthorSearchCommand { AuthorId = _author.Id, Trigger = CommandTrigger.Manual });
Mocker.GetMock<ISearchForReleases>()
Mocker.GetMock<ISearchForNzb>()
.Verify(v => v.AuthorSearch(_author.Id, false, true, false),
Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
}

View File

@@ -16,6 +16,11 @@ namespace NzbDrone.Core.Test.MediaCoverTests
[SetUp]
public void SetUp()
{
if (PlatformInfo.IsMono && PlatformInfo.GetVersion() < new Version(5, 8))
{
Assert.Inconclusive("Not supported on Mono < 5.8");
}
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns<string>(s => File.Exists(s));

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using FizzWare.NBuilder;
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);
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.Test.Framework;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
{
@@ -56,7 +55,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
[SetUp]
public void Setup()
{
_diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual);
_diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider");
Mocker.SetConstant<IDiskProvider>(_diskProvider);
Mocker.GetMock<IConfigService>()
.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("B0192CTMYG", 61209488)]
[TestCase("B0192CTMYG", 42844155)]
[TestCase("9780439554930", 48517161)]
public void successful_book_search(string title, int expected)
{

View File

@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using FluentAssertions.Equivalency;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
@@ -23,13 +21,6 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
[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
{
Name = "Alien Ant Farm",
@@ -152,7 +143,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks();
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
result.Should().BeEquivalentTo(_books.Take(1));
}
[Test]
@@ -161,11 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
GivenMultipleBooks();
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 FizzWare.NBuilder;
using FluentAssertions;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
@@ -147,14 +145,7 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
_authorRepo.Insert(author1);
Action insertDupe = () => _authorRepo.Insert(author2);
if (Db.DatabaseType == DatabaseType.PostgreSQL)
{
insertDupe.Should().Throw<PostgresException>();
}
else
{
insertDupe.Should().Throw<SQLiteException>();
}
insertDupe.Should().Throw<SQLiteException>();
}
}
}

View File

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

View File

@@ -7,6 +7,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ParserTests
{
[TestFixture]
public class QualityParserFixture : CoreTest
{
public static object[] SelfQualityParserCases =

View File

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

View File

@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
public void should_return_with_warning_if_updater_doesnt_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
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;
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
}
}
private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
.Select(@"""Authors"".""Id"" AS ""AuthorId"",
""Books"".""Id"" AS ""BookId"",
SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"",
1 AS ""TotalBookCount"",
CASE WHEN MIN(""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 MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""")
private SqlBuilder Builder() => new SqlBuilder()
.Select(@"Authors.Id AS AuthorId,
Books.Id AS BookId,
SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk,
1 AS TotalBookCount,
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount,
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 BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount")
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)

View File

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

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