mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-08 14:50:02 -04:00
Compare commits
94 Commits
changelog-
...
redirect-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70bc943c17 | ||
|
|
55ab909fde | ||
|
|
61c9779022 | ||
|
|
8299c8e13e | ||
|
|
da5e35fc25 | ||
|
|
a6a2219bc4 | ||
|
|
aa2855a62b | ||
|
|
599f52e72f | ||
|
|
c5c2b94b9a | ||
|
|
b016b36435 | ||
|
|
d93a0c27e9 | ||
|
|
eecf08e063 | ||
|
|
fb1643f630 | ||
|
|
a061179f6b | ||
|
|
b441f6c05b | ||
|
|
0904eac300 | ||
|
|
29e3d8f477 | ||
|
|
71bd88e531 | ||
|
|
0689bec779 | ||
|
|
117a5c8010 | ||
|
|
d10d91439f | ||
|
|
ed0722bae4 | ||
|
|
e69371deca | ||
|
|
5ae8deb9d6 | ||
|
|
291674fc18 | ||
|
|
774cd04d32 | ||
|
|
3eb0533b17 | ||
|
|
2fe11ca1a9 | ||
|
|
c8ae6b0299 | ||
|
|
2086ae1e2c | ||
|
|
2aeec97d5e | ||
|
|
45e9d14916 | ||
|
|
ded45a53f3 | ||
|
|
d5cbb8f84f | ||
|
|
cac0fca0d2 | ||
|
|
9b5a050c40 | ||
|
|
9fa67dbe5f | ||
|
|
505ef5ee63 | ||
|
|
87a2d7382e | ||
|
|
7c74a8df01 | ||
|
|
f8324b3c92 | ||
|
|
cb03f3bf6b | ||
|
|
d6f85ec4f9 | ||
|
|
dd0dd921b0 | ||
|
|
1f02423148 | ||
|
|
7a4fba851f | ||
|
|
2d5efc268f | ||
|
|
cad5762f31 | ||
|
|
bcc8370d05 | ||
|
|
3481168df5 | ||
|
|
337a30ac0f | ||
|
|
eaad4de4dc | ||
|
|
b98ae37abf | ||
|
|
5dac84f9d8 | ||
|
|
ef21c73619 | ||
|
|
14d74f2eca | ||
|
|
c3cbbb7627 | ||
|
|
ad3a58c422 | ||
|
|
347b154882 | ||
|
|
63ccc155d6 | ||
|
|
76613316fa | ||
|
|
f33e9f2bbc | ||
|
|
e4a3d7b273 | ||
|
|
46c2e0ba82 | ||
|
|
8616373f96 | ||
|
|
b3f99d8c20 | ||
|
|
c0c0847963 | ||
|
|
c92f9d03c0 | ||
|
|
f7bf1e243d | ||
|
|
20b1f41ac1 | ||
|
|
f703db1e00 | ||
|
|
20f67c8035 | ||
|
|
56ae497bfa | ||
|
|
e760dc56c6 | ||
|
|
7ad26b386c | ||
|
|
199085249f | ||
|
|
b84041d95f | ||
|
|
615facb3c4 | ||
|
|
b4fd1340c2 | ||
|
|
1da27b0978 | ||
|
|
b0a3ddef9c | ||
|
|
a59706ceb4 | ||
|
|
ce58e6ecdb | ||
|
|
d01ce8b908 | ||
|
|
575f995916 | ||
|
|
c17c8815ef | ||
|
|
8445941510 | ||
|
|
32ae18ae6f | ||
|
|
b91ec241a6 | ||
|
|
17ce8187fd | ||
|
|
41615fe026 | ||
|
|
9cff8f31e9 | ||
|
|
21538b972d | ||
|
|
5a7b4d41d8 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
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.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -42,12 +42,14 @@ 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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
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.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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
|
||||
@@ -62,6 +62,15 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
||||
|
||||
[](https://opencollective.com/readarr#mega-sponsor)
|
||||
|
||||
## DigitalOcean
|
||||
|
||||
This project is also supported by DigitalOcean
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
|
||||
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -15,7 +15,7 @@ variables:
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.201'
|
||||
dotnetVersion: '6.0.302'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
@@ -66,16 +66,13 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
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
|
||||
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
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
@@ -87,29 +84,27 @@ 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
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
path: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
artifactType: 'pipeline'
|
||||
parallel: true
|
||||
parallelCount: 100
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
|
||||
- stage: Build_Backend_Other
|
||||
displayName: Build Backend (Other OS)
|
||||
@@ -141,25 +136,29 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
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
|
||||
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
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
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
|
||||
@@ -262,35 +261,35 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
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 Windows Core zip
|
||||
displayName: Create win-x64 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 Windows x86 Core zip
|
||||
displayName: Create win-x86 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 MacOS x64 Core app
|
||||
displayName: Create osx-x64 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 MacOS x64 Core tar
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -298,14 +297,14 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core app
|
||||
displayName: Create osx-arm64 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 MacOS arm64 Core tar
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -313,7 +312,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -321,7 +320,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -329,7 +328,15 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
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
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -337,7 +344,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -345,7 +352,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux arm64 Core tar
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -353,7 +360,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -361,7 +368,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -413,22 +420,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.macImage }}
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'FreebsdCore'
|
||||
testName: 'freebsd-x64'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -447,7 +454,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
|
||||
@@ -475,8 +482,12 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
artifactName: linux-musl-x64-tests
|
||||
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 }}
|
||||
@@ -487,9 +498,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
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
|
||||
@@ -512,6 +529,57 @@ 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
|
||||
@@ -523,17 +591,17 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
@@ -550,7 +618,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -580,6 +648,66 @@ 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:
|
||||
@@ -595,14 +723,14 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
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
|
||||
@@ -629,11 +757,15 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
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 }}
|
||||
|
||||
@@ -643,9 +775,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
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
|
||||
@@ -691,14 +829,17 @@ 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'
|
||||
|
||||
@@ -715,7 +856,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -909,3 +1050,4 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
45
build.sh
45
build.sh
@@ -27,15 +27,22 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableBsdSupport()
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
#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"
|
||||
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
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
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
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -292,7 +299,8 @@ if [ $# -eq 0 ]; then
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -304,8 +312,12 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -349,12 +361,17 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableBsdSupport
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
@@ -364,9 +381,10 @@ then
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -405,9 +423,10 @@ then
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'filenames',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
@@ -308,7 +309,9 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 2,
|
||||
'react/jsx-wrap-multilines': 2
|
||||
'react/jsx-wrap-multilines': 2,
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -169,6 +169,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -8,17 +8,17 @@ function AuthorMonitorNewItemsOptionsPopoverContent() {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data="Monitor all new books"
|
||||
data={translate('DataNewAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewBooks')}
|
||||
data="Monitor new books released after the newest existing book"
|
||||
data={translate('DataNewBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="Don't monitor any new books"
|
||||
data={translate('DataNewNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -8,42 +8,42 @@ function AuthorMonitoringOptionsPopoverContent() {
|
||||
return (
|
||||
<>
|
||||
<Alert>
|
||||
This is a one time adjustment to set which books are monitored
|
||||
{translate('MonitoringOptionsHelpText')}
|
||||
</Alert>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data="Monitor all books"
|
||||
data={translate('DataAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data="Monitor books that have not released yet"
|
||||
data={translate('DataFutureBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data="Monitor books that do not have files or have not released yet"
|
||||
data={translate('DataMissingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data="Monitor books that have files or have not released yet"
|
||||
data={translate('DataExistingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data="Monitor the first book. All other books will be ignored"
|
||||
data={translate('DataFirstBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data="Monitor the latest book and future books"
|
||||
data={translate('DataLatestBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="No books will be monitored"
|
||||
data={translate('DataNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</>
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Readarr">
|
||||
<DocumentTitle title={window.Readarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -267,7 +267,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onOrganizeAuthorPress}
|
||||
>
|
||||
Rename Files
|
||||
{translate('RenameFiles')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -277,7 +277,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onRetagAuthorPress}
|
||||
>
|
||||
Write Metadata Tags
|
||||
{translate('WriteMetadataTags')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -286,7 +286,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Readarr Tags
|
||||
{translate('SetReadarrTags')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -296,7 +296,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -43,11 +44,12 @@ function createMapStateToProps() {
|
||||
(state, { titleSlug }) => titleSlug,
|
||||
selectBookFiles,
|
||||
(state) => state.books,
|
||||
(state) => state.editions,
|
||||
createAllAuthorSelector(),
|
||||
createCommandsSelector(),
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
|
||||
(titleSlug, bookFiles, books, editions, 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);
|
||||
@@ -79,8 +81,8 @@ function createMapStateToProps() {
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
);
|
||||
|
||||
const isFetching = isBookFilesFetching;
|
||||
const isPopulated = isBookFilesPopulated;
|
||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||
const isPopulated = isBookFilesPopulated && editions.isPopulated;
|
||||
|
||||
return {
|
||||
...book,
|
||||
@@ -104,6 +106,8 @@ const mapDispatchToProps = {
|
||||
executeCommand,
|
||||
fetchBookFiles,
|
||||
clearBookFiles,
|
||||
fetchEditions,
|
||||
clearEditions,
|
||||
clearReleases,
|
||||
cancelFetchReleases,
|
||||
toggleBooksMonitored
|
||||
@@ -121,7 +125,8 @@ class BookDetailsConnector extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
if (prevProps.id !== this.props.id ||
|
||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
@@ -140,12 +145,14 @@ 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();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -195,6 +202,8 @@ 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,
|
||||
|
||||
@@ -8,15 +8,25 @@ 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, uiSettings, dimensions) => {
|
||||
(book, overview, uiSettings, dimensions) => {
|
||||
|
||||
return {
|
||||
...book,
|
||||
overview,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ class EditBookModalContent extends Component {
|
||||
editions
|
||||
} = item;
|
||||
|
||||
const hasFile = statistics ? statistics.bookFileCount : 0;
|
||||
const hasFile = statistics ? statistics.bookFileCount > 0 : false;
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load editions');
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
|
||||
import { saveEditions } from 'Store/Actions/editionActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createBookSelector from 'Store/Selectors/createBookSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
@@ -26,17 +26,14 @@ function createMapStateToProps() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
error
|
||||
} = editionState;
|
||||
|
||||
book.editions = items;
|
||||
|
||||
const bookSettings = _.pick(book, [
|
||||
'monitored',
|
||||
'anyEditionOk',
|
||||
'editions'
|
||||
'anyEditionOk'
|
||||
]);
|
||||
bookSettings.editions = editionState.items;
|
||||
|
||||
const settings = selectSettings(bookSettings, pendingChanges, saveError);
|
||||
|
||||
@@ -58,10 +55,9 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchEditions: fetchEditions,
|
||||
dispatchClearEditions: clearEditions,
|
||||
dispatchSetBookValue: setBookValue,
|
||||
dispatchSaveBook: saveBook
|
||||
dispatchSaveBook: saveBook,
|
||||
dispatchSaveEditions: saveEditions
|
||||
};
|
||||
|
||||
class EditBookModalContentConnector extends Component {
|
||||
@@ -69,20 +65,12 @@ 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
|
||||
|
||||
@@ -94,6 +82,9 @@ class EditBookModalContentConnector extends Component {
|
||||
this.props.dispatchSaveBook({
|
||||
id: this.props.bookId
|
||||
});
|
||||
this.props.dispatchSaveEditions({
|
||||
id: this.props.bookId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
@@ -114,10 +105,9 @@ 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
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -32,11 +33,13 @@ function selectShowSearchAction() {
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookSelector(),
|
||||
createBookAuthorSelector(),
|
||||
createBookQualityProfileSelector(),
|
||||
selectShowSearchAction(),
|
||||
createExecutingCommandsSelector(),
|
||||
(
|
||||
book,
|
||||
author,
|
||||
qualityProfile,
|
||||
showSearchAction,
|
||||
executingCommands
|
||||
@@ -54,7 +57,7 @@ function createMapStateToProps() {
|
||||
const isRefreshingBook = executingCommands.some((command) => {
|
||||
return (
|
||||
(command.name === commandNames.REFRESH_AUTHOR &&
|
||||
command.body.authorId === book.author.id) ||
|
||||
command.body.authorId === book.authorId) ||
|
||||
(command.name === commandNames.REFRESH_BOOK &&
|
||||
command.body.bookId === book.id)
|
||||
);
|
||||
@@ -63,7 +66,7 @@ function createMapStateToProps() {
|
||||
const isSearchingBook = executingCommands.some((command) => {
|
||||
return (
|
||||
(command.name === commandNames.AUTHOR_SEARCH &&
|
||||
command.body.authorId === book.author.id) ||
|
||||
command.body.authorId === book.authorId) ||
|
||||
(command.name === commandNames.BOOK_SEARCH &&
|
||||
command.body.bookIds.includes(book.id))
|
||||
);
|
||||
@@ -71,6 +74,7 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
...book,
|
||||
author,
|
||||
qualityProfile,
|
||||
showSearchAction,
|
||||
isRefreshingBook,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
@@ -42,10 +43,26 @@ class BookIndexOverview extends Component {
|
||||
|
||||
this.state = {
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: false
|
||||
isDeleteAuthorModalOpen: false,
|
||||
overview: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { id } = this.props;
|
||||
|
||||
// Note that this component is lazy loaded by the virtualised view.
|
||||
// We want to avoid storing overviews for *all* books which is
|
||||
// why it's not put into the redux store
|
||||
const promise = createAjaxRequest({
|
||||
url: `/book/${id}/overview`
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
this.setState({ overview: data.overview });
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -84,7 +101,6 @@ class BookIndexOverview extends Component {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
overview,
|
||||
monitored,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
@@ -118,6 +134,7 @@ class BookIndexOverview extends Component {
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
overview,
|
||||
isEditAuthorModalOpen,
|
||||
isDeleteAuthorModalOpen
|
||||
} = this.state;
|
||||
@@ -267,7 +284,6 @@ 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,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -114,7 +115,7 @@ class BookshelfFooter extends Component {
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor Existing Books
|
||||
{translate('MonitorExistingBooks')}
|
||||
</div>
|
||||
|
||||
<MonitorBooksSelectInput
|
||||
@@ -128,7 +129,7 @@ class BookshelfFooter extends Component {
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor New Books
|
||||
{translate('MonitorNewBooks')}
|
||||
</div>
|
||||
|
||||
<MonitorNewItemsSelectInput
|
||||
@@ -152,7 +153,7 @@ class BookshelfFooter extends Component {
|
||||
isDisabled={!selectedCount || noChanges}
|
||||
onPress={this.onUpdateSelectedPress}
|
||||
>
|
||||
Update Selected
|
||||
{translate('UpdateSelected')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -216,7 +217,7 @@ function FormInputGroup(props) {
|
||||
<Link
|
||||
to={helpLink}
|
||||
>
|
||||
More Info
|
||||
{translate('MoreInfo')}
|
||||
</Link>
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function ConfirmModal(props) {
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [isOpen, onConfirm]);
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={PageContentError}>
|
||||
<DocumentTitle title={title ? `${title} - Readarr` : 'Readarr'}>
|
||||
<DocumentTitle title={title ? `${title} - ${window.Readarr.instanceName}` : window.Readarr.instanceName}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
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
|
||||
|
||||
@@ -41,10 +30,8 @@ class PageContentBody extends Component {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
@@ -53,7 +40,7 @@ class PageContentBody extends Component {
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
</div>
|
||||
</ScrollerComponent>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.jumpBar {
|
||||
z-index: $pageJumpBarZIndex;
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './OverlayScroller.css';
|
||||
|
||||
|
||||
@@ -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/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import styles from './SwipeHeader.css';
|
||||
|
||||
function cursorPosition(event) {
|
||||
|
||||
@@ -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/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
let maxWidth = null;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -9,7 +10,8 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import SelectEditionRow from './SelectEditionRow';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectEditionRowConnector from './SelectEditionRowConnector';
|
||||
import styles from './SelectEditionModalContent.css';
|
||||
|
||||
const columns = [
|
||||
@@ -33,15 +35,30 @@ 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>
|
||||
Manual Import - Select Edition
|
||||
{translate('ManualImportSelectEdition')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -60,7 +77,7 @@ class SelectEditionModalContent extends Component {
|
||||
{
|
||||
books.map((item) => {
|
||||
return (
|
||||
<SelectEditionRow
|
||||
<SelectEditionRowConnector
|
||||
key={item.book.id}
|
||||
matchedEditionId={item.matchedEditionId}
|
||||
columns={columns}
|
||||
@@ -76,7 +93,7 @@ class SelectEditionModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
@@ -86,6 +103,9 @@ 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
|
||||
};
|
||||
|
||||
@@ -1,27 +1,71 @@
|
||||
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 {};
|
||||
return createSelector(
|
||||
(state) => state.editions,
|
||||
(editions) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = editions;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -55,6 +99,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
@@ -148,11 +148,10 @@ 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 edition && edition.id !== 0}
|
||||
isExistingBook={'id' in book && book.id !== 0}
|
||||
isExistingAuthor={'id' in book.author && book.author.id !== 0}
|
||||
{...book}
|
||||
/>
|
||||
|
||||
@@ -138,17 +138,20 @@ class AddNewBookSearchResult extends Component {
|
||||
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>
|
||||
{
|
||||
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
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,7 +221,7 @@ AddNewBookSearchResult.propTypes = {
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
author: PropTypes.object,
|
||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
editions: PropTypes.arrayOf(PropTypes.object),
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingBook: PropTypes.bool.isRequired,
|
||||
isExistingAuthor: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -126,7 +126,7 @@ class AddAuthorOptionsForm extends Component {
|
||||
|
||||
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
|
||||
<FormLabel>
|
||||
Metadata Profile
|
||||
{translate('MetadataProfile')}
|
||||
|
||||
{
|
||||
includeNoneMetadataProfile &&
|
||||
@@ -173,7 +173,7 @@ class AddAuthorOptionsForm extends Component {
|
||||
AddAuthorOptionsForm.propTypes = {
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
monitorNewItems: PropTypes.string.isRequired,
|
||||
monitorNewItems: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
metadataProfileId: PropTypes.object,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -20,6 +20,8 @@ function HostSettings(props) {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
applicationUrl,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
@@ -57,6 +59,7 @@ function HostSettings(props) {
|
||||
name="port"
|
||||
min={1}
|
||||
max={65535}
|
||||
autocomplete="off"
|
||||
helpTextWarning={translate('PortHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...port}
|
||||
@@ -78,6 +81,37 @@ 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}
|
||||
>
|
||||
<FormLabel>{translate('ApplicationURL')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="applicationUrl"
|
||||
helpText={translate('ApplicationUrlHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...applicationUrl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -28,17 +28,17 @@ function ImportListMonitoringOptionsPopoverContent() {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="Do not monitor authors or books"
|
||||
data={translate('DataListMonitorNone')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('SpecificBook')}
|
||||
data="Monitor authors but only monitor books explicitly included in the list"
|
||||
data={translate('DataListMonitorSpecificBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AllAuthorBooks')}
|
||||
data="Monitor authors and all books for each author included on the import list"
|
||||
data={translate('DataListMonitorAll')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -89,7 +89,7 @@ function EditImportListModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? 'Edit List' : 'Add List'}
|
||||
{id ? translate('EditList') : translate('AddList')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -148,7 +148,7 @@ function EditImportListModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Monitor
|
||||
{translate('Monitor')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
@@ -318,7 +318,7 @@ function EditImportListModalContent(props) {
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteImportListPress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -327,13 +327,13 @@ function EditImportListModalContent(props) {
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
Test
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
@@ -341,7 +341,7 @@ function EditImportListModalContent(props) {
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
Save
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -346,7 +346,7 @@ function EditRootFolderModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Convert to format
|
||||
{translate('ConvertToFormat')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
@@ -371,7 +371,7 @@ function EditRootFolderModalContent(props) {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Calibre Output Profile
|
||||
{translate('CalibreOutputProfile')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
@@ -423,14 +423,14 @@ function EditRootFolderModalContent(props) {
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteRootFolderPress}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
@@ -438,7 +438,7 @@ function EditRootFolderModalContent(props) {
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
Save
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -19,7 +19,7 @@ function PendingChangesModal(props) {
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [onConfirm]);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -6,6 +6,7 @@ import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { removeItem, set, updateItem } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastSaveData = null;
|
||||
|
||||
export function createCancelSaveProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -27,27 +28,33 @@ function createSaveProviderHandler(section, url, options = {}, removeStale = fal
|
||||
} = payload;
|
||||
|
||||
const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section);
|
||||
const requestUrl = id ? `${url}/${id}` : url;
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-saving the same provider without changes
|
||||
// force it to be saved. Only applies to editing existing providers.
|
||||
|
||||
if (id && _.isEqual(saveData, lastSaveData)) {
|
||||
params.forceSave = true;
|
||||
}
|
||||
|
||||
lastSaveData = saveData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
method: 'POST',
|
||||
url: `${requestUrl}?${$.param(params, true)}`,
|
||||
method: id ? 'PUT' : 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(saveData)
|
||||
};
|
||||
|
||||
if (id) {
|
||||
ajaxOptions.method = 'PUT';
|
||||
if (!Array.isArray(id)) {
|
||||
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastSaveData = null;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
@@ -78,12 +78,8 @@ export const actionHandlers = handleThunks({
|
||||
} = payload;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/history/failed',
|
||||
method: 'POST',
|
||||
data: {
|
||||
id: historyId
|
||||
},
|
||||
dataType: 'json'
|
||||
url: `/history/failed/${historyId}`,
|
||||
method: 'POST'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
|
||||
@@ -86,12 +86,8 @@ export const actionHandlers = handleThunks({
|
||||
} = payload;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/history/failed',
|
||||
method: 'POST',
|
||||
data: {
|
||||
id: historyId
|
||||
},
|
||||
dataType: 'json'
|
||||
url: `/history/failed/${historyId}`,
|
||||
method: 'POST'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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';
|
||||
@@ -25,18 +28,39 @@ 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')
|
||||
[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
|
||||
});
|
||||
})
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -71,6 +71,11 @@ export const defaultState = {
|
||||
label: 'Release Group',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
columnLabel: 'Details',
|
||||
@@ -246,11 +251,8 @@ export const actionHandlers = handleThunks({
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/history/failed',
|
||||
url: `/history/failed/${id}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
id
|
||||
},
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
|
||||
@@ -177,7 +177,8 @@ export const defaultState = {
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'seeders',
|
||||
|
||||
15
frontend/src/Store/Selectors/createBookAuthorSelector.js
Normal file
15
frontend/src/Store/Selectors/createBookAuthorSelector.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import createClientSideCollectionSelector from './createClientSideCollectionSelector';
|
||||
import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector';
|
||||
|
||||
function createUnoptimizedSelector(uiSection) {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('books', uiSection),
|
||||
createBooksClientSideCollectionSelector(uiSection),
|
||||
(books) => {
|
||||
const items = books.items.map((s) => {
|
||||
const {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import createBookSelector from './createBookSelector';
|
||||
import createBookAuthorSelector from './createBookAuthorSelector';
|
||||
|
||||
function createBookQualityProfileSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
createBookSelector(),
|
||||
(qualityProfiles, book) => {
|
||||
if (!book) {
|
||||
createBookAuthorSelector(),
|
||||
(qualityProfiles, author) => {
|
||||
if (!author) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return qualityProfiles.find((profile) => {
|
||||
return profile.id === book.author.qualityProfileId;
|
||||
});
|
||||
return qualityProfiles.find((profile) => profile.id === author.qualityProfileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
@@ -1,123 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
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;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
import filterCollection from 'Utilities/Array/filterCollection';
|
||||
import sortCollection from 'Utilities/Array/sortCollection';
|
||||
import createCustomFiltersSelector from './createCustomFiltersSelector';
|
||||
|
||||
function createClientSideCollectionSelector(section, uiSection) {
|
||||
return createSelector(
|
||||
@@ -127,8 +12,8 @@ function createClientSideCollectionSelector(section, uiSection) {
|
||||
(sectionState, uiSectionState = {}, customFilters) => {
|
||||
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
||||
|
||||
const filtered = filter(state.items, state);
|
||||
const sorted = sort(filtered, state);
|
||||
const filtered = filterCollection(state.items, state);
|
||||
const sorted = sortCollection(filtered, state);
|
||||
|
||||
return {
|
||||
...sectionState,
|
||||
|
||||
14
frontend/src/Store/Selectors/createCustomFiltersSelector.js
Normal file
14
frontend/src/Store/Selectors/createCustomFiltersSelector.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
@@ -1,4 +1,7 @@
|
||||
@define-mixin scrollbar {
|
||||
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
pageJumpBarZIndex: 10,
|
||||
modalZIndex: 1000,
|
||||
popperZIndex: 2000
|
||||
};
|
||||
|
||||
@@ -20,10 +20,11 @@ class About extends Component {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isMono,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -48,14 +49,6 @@ class About extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<DescriptionListItem
|
||||
title={translate('MonoVersion')}
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
@@ -77,6 +70,11 @@ class About extends Component {
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -114,10 +112,11 @@ 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,
|
||||
|
||||
72
frontend/src/Utilities/Array/filterCollection.js
Normal file
72
frontend/src/Utilities/Array/filterCollection.js
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
42
frontend/src/Utilities/Array/sortCollection.js
Normal file
42
frontend/src/Utilities/Array/sortCollection.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
@@ -1,22 +1,18 @@
|
||||
import $ from 'jquery';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
|
||||
function getTranslations() {
|
||||
let localization = null;
|
||||
const ajaxOptions = {
|
||||
async: false,
|
||||
type: 'GET',
|
||||
global: false,
|
||||
dataType: 'json',
|
||||
url: `${window.Readarr.apiRoot}/localization`,
|
||||
url: '/localization',
|
||||
success: function(data) {
|
||||
localization = data.Strings;
|
||||
}
|
||||
};
|
||||
|
||||
ajaxOptions.headers = ajaxOptions.headers || {};
|
||||
ajaxOptions.headers['X-Api-Key'] = window.Readarr.apiKey;
|
||||
createAjaxRequest(ajaxOptions);
|
||||
|
||||
$.ajax(ajaxOptions);
|
||||
return localization;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,7 @@ export function isMobile() {
|
||||
export function isIOS() {
|
||||
return mobileDetect.is('iOS');
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
|
||||
}
|
||||
@@ -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.3",
|
||||
"@microsoft/signalr": "6.0.7",
|
||||
"@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": "4.2.1",
|
||||
"react-custom-scrollbars-2": "4.5.0",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
@@ -109,6 +109,7 @@
|
||||
"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",
|
||||
|
||||
@@ -4,60 +4,62 @@
|
||||
<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="4.8.7" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="5.2.0" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" />
|
||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" />
|
||||
<PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" />
|
||||
<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="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.1.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
|
||||
<PackageVersion Include="Mailkit" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" />
|
||||
<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.1.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr18" />
|
||||
<PackageVersion Include="MonoTorrent" Version="2.0.4" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||
<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.Extensions.Logging" Version="5.1.0" />
|
||||
<PackageVersion Include="NLog" Version="5.0.5" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageVersion Include="Npgsql" Version="6.0.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.2" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.13" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||
<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.14.1" />
|
||||
<PackageVersion Include="Sentry" Version="3.20.1" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<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="16.1.20" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="16.1.20" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.4" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||
<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.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.5" />
|
||||
<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>
|
||||
@@ -1,12 +1,33 @@
|
||||
<?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>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
|
||||
|
||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -10,6 +11,12 @@ 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()
|
||||
{
|
||||
|
||||
@@ -437,24 +437,6 @@ 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()
|
||||
{
|
||||
@@ -540,26 +522,6 @@ 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()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -10,6 +11,12 @@ 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()
|
||||
{
|
||||
|
||||
@@ -207,6 +207,7 @@ 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");
|
||||
@@ -307,11 +308,6 @@ 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();
|
||||
|
||||
@@ -52,6 +52,26 @@ namespace NzbDrone.Common.Test.Http
|
||||
newUri.FullUri.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("", "./relative", "relative")]
|
||||
[TestCase("/", "./relative", "/relative")]
|
||||
[TestCase("/base", "./relative", "/relative")]
|
||||
[TestCase("/base/sub", "./relative", "/base/relative")]
|
||||
[TestCase("/base/sub/", "./relative", "/base/sub/relative")]
|
||||
[TestCase("base/sub", "./relative", "base/relative")]
|
||||
[TestCase("base/sub/", "./relative", "base/sub/relative")]
|
||||
[TestCase("", "../relative", "relative")]
|
||||
[TestCase("/", "../relative", "/relative")]
|
||||
[TestCase("/base", "../relative", "/relative")]
|
||||
[TestCase("/base/sub", "../relative", "/base/relative")]
|
||||
[TestCase("/base/sub/", "../relative", "/base/sub/relative")]
|
||||
[TestCase("base/sub", "../relative", "base/relative")]
|
||||
[TestCase("base/sub/", "../relative", "base/sub/relative")]
|
||||
public void should_combine_uri_with_dot_segment(string basePath, string relativePath, string expected)
|
||||
{
|
||||
var newUri = new HttpUri(basePath) + new HttpUri(relativePath);
|
||||
newUri.FullUri.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("", "", "")]
|
||||
[TestCase("/", "", "/")]
|
||||
[TestCase("base", "", "base")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
@@ -60,6 +60,8 @@ 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""}")]
|
||||
@@ -70,16 +72,41 @@ 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);
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace NzbDrone.Common.Test
|
||||
[Test]
|
||||
public void GetUpdateClientExePath()
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update.exe".AsOsAgnostic());
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update".AsOsAgnostic().ProcessNameToExe());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -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 || PlatformInfo.IsMono)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
suffix = ".exe";
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ 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;
|
||||
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
|
||||
.AddDummyDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
||||
"/boot",
|
||||
"/lib",
|
||||
"/sbin",
|
||||
"/proc"
|
||||
"/proc",
|
||||
"/usr/bin"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
public enum PlatformType
|
||||
{
|
||||
DotNet = 0,
|
||||
Mono = 1,
|
||||
NetCore = 2
|
||||
}
|
||||
|
||||
public interface IPlatformInfo
|
||||
{
|
||||
Version Version { get; }
|
||||
@@ -19,38 +9,18 @@ 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
|
||||
{
|
||||
if (IsDotNet)
|
||||
{
|
||||
return ".NET";
|
||||
}
|
||||
else if (IsMono)
|
||||
{
|
||||
return "Mono";
|
||||
}
|
||||
else
|
||||
{
|
||||
return ".NET Core";
|
||||
}
|
||||
return ".NET";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,107 +30,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +240,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName, PlatformType runtime)
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
processName += ".exe";
|
||||
}
|
||||
@@ -250,11 +250,6 @@ 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();
|
||||
@@ -347,9 +342,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
|
||||
}
|
||||
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
|
||||
}
|
||||
|
||||
public static string GetDatabase(this IAppFolderInfo appFolderInfo)
|
||||
|
||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Common.Http
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||
StatusCode == HttpStatusCode.SeeOther;
|
||||
StatusCode == HttpStatusCode.SeeOther ||
|
||||
StatusCode == HttpStatusCode.PermanentRedirect;
|
||||
|
||||
public string[] GetCookieHeaders()
|
||||
{
|
||||
|
||||
@@ -164,6 +164,37 @@ namespace NzbDrone.Common.Http
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
if (relativePath.StartsWith("./"))
|
||||
{
|
||||
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||
|
||||
var lastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||
}
|
||||
}
|
||||
|
||||
if (relativePath.StartsWith("../"))
|
||||
{
|
||||
relativePath = relativePath.TrimStart('.').TrimStart('/');
|
||||
|
||||
var lastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, lastIndex) + "/";
|
||||
}
|
||||
|
||||
var secondLastIndex = basePath.LastIndexOf("/");
|
||||
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
basePath = basePath.Substring(0, secondLastIndex) + "/";
|
||||
}
|
||||
}
|
||||
|
||||
var baseSlashIndex = basePath.LastIndexOf('/');
|
||||
|
||||
if (baseSlashIndex >= 0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -11,13 +11,14 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&|: )((?:api|auth|pass)?key|(?:access[-_]?)?token|auth|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"),
|
||||
@@ -46,7 +47,14 @@ 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)
|
||||
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)
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
@@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||
|
||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return logBuilder.Property("Sentry", fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||
}
|
||||
|
||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
{
|
||||
SentryLogger.Log(level)
|
||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
||||
SentryLogger.ForLogEvent(level)
|
||||
.CopyLogEvent(logBuilder.LogEvent)
|
||||
.SentryFingerprint(fingerprint)
|
||||
.Write();
|
||||
.Log();
|
||||
|
||||
return logBuilder.Property("Sentry", null);
|
||||
return logBuilder.Property<string>("Sentry", null);
|
||||
}
|
||||
|
||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
||||
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||
{
|
||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
||||
.TimeStamp(logEvent.TimeStamp)
|
||||
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||
.Message(logEvent.Message, logEvent.Parameters)
|
||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
||||
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||
.Exception(logEvent.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,6 @@ 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.");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using NLog;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class NzbDroneFileTarget : FileTarget
|
||||
{
|
||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||
{
|
||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
target.Append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||
|
||||
RegisterGlobalFilters();
|
||||
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
RegisterDebugger();
|
||||
@@ -101,6 +103,16 @@ namespace NzbDrone.Common.Instrumentation
|
||||
LogManager.Configuration.LoggingRules.Add(loggingRule);
|
||||
}
|
||||
|
||||
private static void RegisterGlobalFilters()
|
||||
{
|
||||
LogManager.Setup().LoadConfiguration(c =>
|
||||
{
|
||||
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterConsole()
|
||||
{
|
||||
var level = LogLevel.Trace;
|
||||
|
||||
@@ -109,13 +109,6 @@ 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;
|
||||
@@ -158,14 +151,6 @@ 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,7 +206,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
if (ex != null)
|
||||
{
|
||||
fingerPrint.Add(ex.GetType().FullName);
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
if (ex.TargetSite != null)
|
||||
{
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
}
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||
|
||||
@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
|
||||
try
|
||||
{
|
||||
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
|
||||
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
|
||||
|
||||
var key = environmentVariable.Key.ToString();
|
||||
var value = environmentVariable.Value?.ToString();
|
||||
|
||||
if (startInfo.EnvironmentVariables.ContainsKey(key))
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.EnvironmentVariables.Add(key, value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -366,11 +377,6 @@ 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}");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()).ToList();
|
||||
var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).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()).ToList();
|
||||
var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).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()
|
||||
new SqlBuilder(db.DatabaseType)
|
||||
.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)
|
||||
|
||||
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
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))");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class WhereBuilderFixture : CoreTest
|
||||
public class WhereBuilderSqliteFixture : CoreTest
|
||||
{
|
||||
private WhereBuilder _subject;
|
||||
private WhereBuilderSqlite _subject;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void MapTables()
|
||||
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
Mocker.Resolve<DbFactory>();
|
||||
}
|
||||
|
||||
private WhereBuilder Where(Expression<Func<Author, bool>> filter)
|
||||
private WhereBuilderSqlite Where(Expression<Func<Author, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(filter, true, 0);
|
||||
}
|
||||
|
||||
private WhereBuilder WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
private WhereBuilderSqlite WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(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 WhereBuilder(filter, true, 0);
|
||||
_subject = new WhereBuilderSqlite(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 WhereBuilder(filter, false, 0);
|
||||
_subject = new WhereBuilderSqlite(filter, false, 0);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
@@ -5,6 +6,7 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -244,5 +246,89 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repacks_are_not_preferred()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotPrefer);
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "";
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repack_but_auto_download_repacks_is_true()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.PreferAndUpgrade);
|
||||
|
||||
_parsedBookInfo.Quality.Revision.IsRepack = true;
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "Readarr";
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_when_repack_but_auto_download_repacks_is_false()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotUpgrade);
|
||||
|
||||
_parsedBookInfo.Quality.Revision.IsRepack = true;
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "Readarr";
|
||||
return c;
|
||||
}).ToList();
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
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
|
||||
{
|
||||
@@ -47,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
public abstract class DbTest : CoreTest
|
||||
{
|
||||
private ITestDatabase _db;
|
||||
private DatabaseType _databaseType;
|
||||
|
||||
protected virtual MigrationType MigrationType => MigrationType.Main;
|
||||
|
||||
@@ -65,8 +72,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
|
||||
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
||||
{
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
var database = factory.Create(migrationContext);
|
||||
var database = CreateDatabase(migrationContext);
|
||||
Mocker.SetConstant(database);
|
||||
|
||||
switch (MigrationType)
|
||||
@@ -98,6 +104,65 @@ 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);
|
||||
@@ -108,6 +173,13 @@ 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>());
|
||||
|
||||
@@ -127,12 +199,19 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using Unity.Resolution;
|
||||
using NzbDrone.Test.Common.AutoMoq;
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
@@ -14,12 +15,9 @@ namespace NzbDrone.Core.Test.Framework
|
||||
[SetUp]
|
||||
public void FileSystemTestSetup()
|
||||
{
|
||||
FileSystem = new MockFileSystem();
|
||||
FileSystem = (MockFileSystem)Mocker.Resolve<IFileSystem>(FileSystemType.Mock);
|
||||
|
||||
DiskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider", new ResolverOverride[]
|
||||
{
|
||||
new ParameterOverride("fileSystem", FileSystem)
|
||||
});
|
||||
DiskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Mock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
where T : ModelBase, new();
|
||||
IDirectDataMapper GetDirectDataMapper();
|
||||
IDbConnection OpenConnection();
|
||||
DatabaseType DatabaseType { get; }
|
||||
}
|
||||
|
||||
public class TestDatabase : ITestDatabase
|
||||
@@ -30,6 +31,8 @@ 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;
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
.Setup(s => s.GetAuthor(It.IsAny<int>()))
|
||||
.Returns(_author);
|
||||
|
||||
Mocker.GetMock<ISearchForNzb>()
|
||||
Mocker.GetMock<ISearchForReleases>()
|
||||
.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<ISearchForNzb>()
|
||||
Mocker.GetMock<ISearchForReleases>()
|
||||
.Verify(v => v.AuthorSearch(_author.Id, false, true, false),
|
||||
Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ 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));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
@@ -133,6 +135,13 @@ 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);
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
{
|
||||
@@ -55,9 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider");
|
||||
|
||||
Mocker.SetConstant<IDiskProvider>(_diskProvider);
|
||||
_diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual);
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(x => x.WriteAudioTags)
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
}
|
||||
|
||||
[TestCase("Harry Potter and the sorcerer's stone", 3)]
|
||||
[TestCase("B0192CTMYG", 42844155)]
|
||||
[TestCase("B0192CTMYG", 61209488)]
|
||||
[TestCase("9780439554930", 48517161)]
|
||||
public void successful_book_search(string title, int expected)
|
||||
{
|
||||
|
||||
@@ -3,8 +3,10 @@ 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
|
||||
@@ -21,6 +23,13 @@ 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",
|
||||
@@ -143,7 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -152,7 +161,11 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ 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;
|
||||
@@ -145,7 +147,14 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
|
||||
_authorRepo.Insert(author1);
|
||||
|
||||
Action insertDupe = () => _authorRepo.Insert(author2);
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
if (Db.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
insertDupe.Should().Throw<PostgresException>();
|
||||
}
|
||||
else
|
||||
{
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IEnumerable<string>>()))
|
||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<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<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<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<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>()))
|
||||
.Returns(_books);
|
||||
|
||||
// Update called twice for a move/merge
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user