mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-18 21:34:28 -04:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f448481460 | |||
| 70856c217c | |||
| 4db158e0c9 | |||
| 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 |
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
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:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -42,12 +42,14 @@ body:
|
|||||||
- **Docker Install**: Yes
|
- **Docker Install**: Yes
|
||||||
- **Using Reverse Proxy**: No
|
- **Using Reverse Proxy**: No
|
||||||
- **Browser**: Firefox 90 (If UI related)
|
- **Browser**: Firefox 90 (If UI related)
|
||||||
|
- **Database**: Sqlite 3.36.0
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- OS:
|
||||||
- Readarr:
|
- Readarr:
|
||||||
- Docker Install:
|
- Docker Install:
|
||||||
- Using Reverse Proxy:
|
- Using Reverse Proxy:
|
||||||
- Browser:
|
- Browser:
|
||||||
|
- Database:
|
||||||
render: markdown
|
render: markdown
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
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:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -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)
|
[](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
|
### License
|
||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
|
|||||||
@@ -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.
|
||||||
+114
-2
@@ -15,7 +15,7 @@ variables:
|
|||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.300'
|
dotnetVersion: '6.0.302'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
@@ -529,6 +529,57 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
|
- job: Unit_LinuxCore_Postgres
|
||||||
|
displayName: Unit Native LinuxCore with Postgres Database
|
||||||
|
variables:
|
||||||
|
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||||
|
artifactName: LinuxCoreTests
|
||||||
|
Readarr__Postgres__Host: 'localhost'
|
||||||
|
Readarr__Postgres__Port: '5432'
|
||||||
|
Readarr__Postgres__User: 'readarr'
|
||||||
|
Readarr__Postgres__Password: 'readarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: 'linux-x64-Tests'
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||||
|
displayName: Make Test Dummy Executable
|
||||||
|
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres14 \
|
||||||
|
-e POSTGRES_PASSWORD=readarr \
|
||||||
|
-e POSTGRES_USER=readarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:14
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
ls -lR ${TESTSFOLDER}
|
||||||
|
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||||
|
displayName: Run Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
displayName: Publish Test Results
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
@@ -597,6 +648,66 @@ stages:
|
|||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
- job: Integration_LinuxCore_Postgres
|
||||||
|
displayName: Integration Native LinuxCore with Postgres Database
|
||||||
|
variables:
|
||||||
|
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||||
|
Readarr__Postgres__Host: 'localhost'
|
||||||
|
Readarr__Postgres__Port: '5432'
|
||||||
|
Readarr__Postgres__User: 'readarr'
|
||||||
|
Readarr__Postgres__Password: 'readarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: 'linux-x64-tests'
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Build Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: Packages
|
||||||
|
itemPattern: '**/$(pattern)'
|
||||||
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
|
- task: ExtractFiles@1
|
||||||
|
inputs:
|
||||||
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
|
displayName: Extract Package
|
||||||
|
- bash: |
|
||||||
|
mkdir -p ./bin/
|
||||||
|
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
|
||||||
|
displayName: Move Package Contents
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres14 \
|
||||||
|
-e POSTGRES_PASSWORD=readarr \
|
||||||
|
-e POSTGRES_USER=readarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:14
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||||
|
displayName: Run Integration Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
displayName: Integration Native FreeBSD
|
displayName: Integration Native FreeBSD
|
||||||
workspace:
|
workspace:
|
||||||
@@ -619,7 +730,7 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: Packages
|
artifactName: Packages
|
||||||
itemPattern: '/$(pattern)'
|
itemPattern: '**/$(pattern)'
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- bash: |
|
- bash: |
|
||||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||||
@@ -939,3 +1050,4 @@ stages:
|
|||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
'filenames',
|
'filenames',
|
||||||
'react',
|
'react',
|
||||||
|
'react-hooks',
|
||||||
'simple-import-sort',
|
'simple-import-sort',
|
||||||
'import'
|
'import'
|
||||||
],
|
],
|
||||||
@@ -308,7 +309,9 @@ module.exports = {
|
|||||||
'react/react-in-jsx-scope': 2,
|
'react/react-in-jsx-scope': 2,
|
||||||
'react/self-closing-comp': 2,
|
'react/self-closing-comp': 2,
|
||||||
'react/sort-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: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -169,6 +169,16 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'details') {
|
if (name === 'details') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
||||||
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function ConfirmModal(props) {
|
|||||||
|
|
||||||
return () => unbindShortcut('enter', onConfirm);
|
return () => unbindShortcut('enter', onConfirm);
|
||||||
}
|
}
|
||||||
}, [isOpen, onConfirm]);
|
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
|
|||||||
import FocusLock from 'react-focus-lock';
|
import FocusLock from 'react-focus-lock';
|
||||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { isIOS } from 'Utilities/browser';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import { isIOS } from 'Utilities/mobile';
|
|
||||||
import { setScrollLock } from 'Utilities/scrollLock';
|
import { setScrollLock } from 'Utilities/scrollLock';
|
||||||
import ModalError from './ModalError';
|
import ModalError from './ModalError';
|
||||||
import styles from './Modal.css';
|
import styles from './Modal.css';
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
|
||||||
import { isLocked } from 'Utilities/scrollLock';
|
import { isLocked } from 'Utilities/scrollLock';
|
||||||
import styles from './PageContentBody.css';
|
import styles from './PageContentBody.css';
|
||||||
|
|
||||||
class PageContentBody extends Component {
|
class PageContentBody extends Component {
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._isMobile = isMobileUtil();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
@@ -41,10 +30,8 @@ class PageContentBody extends Component {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollerComponent
|
<Scroller
|
||||||
className={className}
|
className={className}
|
||||||
scrollDirection={scrollDirections.VERTICAL}
|
scrollDirection={scrollDirections.VERTICAL}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
@@ -53,7 +40,7 @@ class PageContentBody extends Component {
|
|||||||
<div className={innerClassName}>
|
<div className={innerClassName}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ScrollerComponent>
|
</Scroller>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.jumpBar {
|
.jumpBar {
|
||||||
|
z-index: $pageJumpBarZIndex;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Scrollbars } from 'react-custom-scrollbars';
|
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import styles from './OverlayScroller.css';
|
import styles from './OverlayScroller.css';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Measure from 'Components/Measure';
|
import Measure from 'Components/Measure';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
import styles from './SwipeHeader.css';
|
import styles from './SwipeHeader.css';
|
||||||
|
|
||||||
function cursorPosition(event) {
|
function cursorPosition(event) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Manager, Popper, Reference } from 'react-popper';
|
|||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
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';
|
import styles from './Tooltip.css';
|
||||||
|
|
||||||
let maxWidth = null;
|
let maxWidth = null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
@@ -9,7 +10,8 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import SelectEditionRow from './SelectEditionRow';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectEditionRowConnector from './SelectEditionRowConnector';
|
||||||
import styles from './SelectEditionModalContent.css';
|
import styles from './SelectEditionModalContent.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -33,15 +35,30 @@ class SelectEditionModalContent extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
books,
|
books,
|
||||||
|
isPopulated,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
onEditionSelect,
|
onEditionSelect,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (!isPopulated && !error) {
|
||||||
|
return (<LoadingIndicator />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFetching && error) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{translate('LoadingEditionsFailed')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Edition
|
{translate('ManualImportSelectEdition')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody
|
<ModalBody
|
||||||
@@ -60,7 +77,7 @@ class SelectEditionModalContent extends Component {
|
|||||||
{
|
{
|
||||||
books.map((item) => {
|
books.map((item) => {
|
||||||
return (
|
return (
|
||||||
<SelectEditionRow
|
<SelectEditionRowConnector
|
||||||
key={item.book.id}
|
key={item.book.id}
|
||||||
matchedEditionId={item.matchedEditionId}
|
matchedEditionId={item.matchedEditionId}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -76,7 +93,7 @@ class SelectEditionModalContent extends Component {
|
|||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
Cancel
|
{translate('Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
@@ -86,6 +103,9 @@ class SelectEditionModalContent extends Component {
|
|||||||
|
|
||||||
SelectEditionModalContent.propTypes = {
|
SelectEditionModalContent.propTypes = {
|
||||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isFetching: PropTypes.bool,
|
||||||
|
isPopulated: PropTypes.bool,
|
||||||
|
error: PropTypes.object,
|
||||||
onEditionSelect: PropTypes.func.isRequired,
|
onEditionSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,71 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
|
||||||
import {
|
import {
|
||||||
saveInteractiveImportItem,
|
saveInteractiveImportItem,
|
||||||
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import SelectEditionModalContent from './SelectEditionModalContent';
|
import SelectEditionModalContent from './SelectEditionModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return {};
|
return createSelector(
|
||||||
|
(state) => state.editions,
|
||||||
|
(editions) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error
|
||||||
|
} = editions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
fetchEditions,
|
||||||
|
clearEditions,
|
||||||
updateInteractiveImportItem,
|
updateInteractiveImportItem,
|
||||||
saveInteractiveImportItem
|
saveInteractiveImportItem
|
||||||
};
|
};
|
||||||
|
|
||||||
class SelectEditionModalContentConnector extends Component {
|
class SelectEditionModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
registerPagePopulator(this.populate);
|
||||||
|
this.populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.populate);
|
||||||
|
this.unpopulate();
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
populate = () => {
|
||||||
|
const bookId = this.props.books.map((b) => b.book.id);
|
||||||
|
|
||||||
|
this.props.fetchEditions({ bookId });
|
||||||
|
}
|
||||||
|
|
||||||
|
unpopulate = () => {
|
||||||
|
this.props.clearEditions();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onEditionSelect = (bookId, foreignEditionId) => {
|
onEditionSelect = (bookId, foreignEditionId) => {
|
||||||
console.log(`book: ${bookId} id: ${foreignEditionId} ${typeof foreignEditionId}`);
|
|
||||||
const ids = this.props.importIdsByBook[bookId];
|
const ids = this.props.importIdsByBook[bookId];
|
||||||
|
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
@@ -55,6 +99,8 @@ class SelectEditionModalContentConnector extends Component {
|
|||||||
SelectEditionModalContentConnector.propTypes = {
|
SelectEditionModalContentConnector.propTypes = {
|
||||||
importIdsByBook: PropTypes.object.isRequired,
|
importIdsByBook: PropTypes.object.isRequired,
|
||||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchEditions: PropTypes.func.isRequired,
|
||||||
|
clearEditions: PropTypes.func.isRequired,
|
||||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -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) {
|
} else if (item.book) {
|
||||||
const book = item.book;
|
const book = item.book;
|
||||||
const edition = book.editions.find((x) => x.monitored);
|
|
||||||
return (
|
return (
|
||||||
<AddNewBookSearchResultConnector
|
<AddNewBookSearchResultConnector
|
||||||
key={item.id}
|
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}
|
isExistingAuthor={'id' in book.author && book.author.id !== 0}
|
||||||
{...book}
|
{...book}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -138,17 +138,20 @@ class AddNewBookSearchResult extends Component {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<Link
|
{
|
||||||
className={styles.mbLink}
|
editions && editions.length > 1 ?
|
||||||
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
<Link
|
||||||
onPress={this.onTVDBLinkPress}
|
className={styles.mbLink}
|
||||||
>
|
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
||||||
<Icon
|
onPress={this.onTVDBLinkPress}
|
||||||
className={styles.mbLinkIcon}
|
>
|
||||||
name={icons.EXTERNAL_LINK}
|
<Icon
|
||||||
size={28}
|
className={styles.mbLinkIcon}
|
||||||
/>
|
name={icons.EXTERNAL_LINK}
|
||||||
</Link>
|
size={28}
|
||||||
|
/>
|
||||||
|
</Link> : null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -218,7 +221,7 @@ AddNewBookSearchResult.propTypes = {
|
|||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
author: PropTypes.object,
|
author: PropTypes.object,
|
||||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
editions: PropTypes.arrayOf(PropTypes.object),
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isExistingBook: PropTypes.bool.isRequired,
|
isExistingBook: PropTypes.bool.isRequired,
|
||||||
isExistingAuthor: PropTypes.bool.isRequired,
|
isExistingAuthor: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class AddAuthorOptionsForm extends Component {
|
|||||||
AddAuthorOptionsForm.propTypes = {
|
AddAuthorOptionsForm.propTypes = {
|
||||||
rootFolderPath: PropTypes.object,
|
rootFolderPath: PropTypes.object,
|
||||||
monitor: PropTypes.object.isRequired,
|
monitor: PropTypes.object.isRequired,
|
||||||
monitorNewItems: PropTypes.string.isRequired,
|
monitorNewItems: PropTypes.object.isRequired,
|
||||||
qualityProfileId: PropTypes.object,
|
qualityProfileId: PropTypes.object,
|
||||||
metadataProfileId: PropTypes.object,
|
metadataProfileId: PropTypes.object,
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function HostSettings(props) {
|
|||||||
port,
|
port,
|
||||||
urlBase,
|
urlBase,
|
||||||
instanceName,
|
instanceName,
|
||||||
|
applicationUrl,
|
||||||
enableSsl,
|
enableSsl,
|
||||||
sslPort,
|
sslPort,
|
||||||
sslCertPath,
|
sslCertPath,
|
||||||
@@ -58,6 +59,7 @@ function HostSettings(props) {
|
|||||||
name="port"
|
name="port"
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
|
autocomplete="off"
|
||||||
helpTextWarning={translate('PortHelpTextWarning')}
|
helpTextWarning={translate('PortHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...port}
|
{...port}
|
||||||
@@ -95,6 +97,21 @@ function HostSettings(props) {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('ApplicationURL')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="applicationUrl"
|
||||||
|
helpText={translate('ApplicationUrlHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...applicationUrl}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function PendingChangesModal(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bindShortcut('enter', onConfirm);
|
bindShortcut('enter', onConfirm);
|
||||||
}, [onConfirm]);
|
}, [bindShortcut, onConfirm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import getProviderState from 'Utilities/State/getProviderState';
|
|||||||
import { removeItem, set, updateItem } from '../baseActions';
|
import { removeItem, set, updateItem } from '../baseActions';
|
||||||
|
|
||||||
const abortCurrentRequests = {};
|
const abortCurrentRequests = {};
|
||||||
|
let lastSaveData = null;
|
||||||
|
|
||||||
export function createCancelSaveProviderHandler(section) {
|
export function createCancelSaveProviderHandler(section) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
@@ -27,27 +28,33 @@ function createSaveProviderHandler(section, url, options = {}, removeStale = fal
|
|||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section);
|
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 = {
|
const ajaxOptions = {
|
||||||
url: `${url}?${$.param(queryParams, true)}`,
|
url: `${requestUrl}?${$.param(params, true)}`,
|
||||||
method: 'POST',
|
method: id ? 'PUT' : 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
data: JSON.stringify(saveData)
|
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);
|
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||||
|
|
||||||
abortCurrentRequests[section] = abortRequest;
|
abortCurrentRequests[section] = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
lastSaveData = null;
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
data = [data];
|
data = [data];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export const defaultState = {
|
|||||||
label: 'Release Group',
|
label: 'Release Group',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceTitle',
|
||||||
|
label: 'Source Title',
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
columnLabel: 'Details',
|
columnLabel: 'Details',
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: 'Size',
|
label: 'Size',
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER,
|
||||||
|
valueType: filterBuilderValueTypes.BYTES
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'seeders',
|
name: 'seeders',
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
import createClientSideCollectionSelector from './createClientSideCollectionSelector';
|
import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector';
|
||||||
|
|
||||||
function createUnoptimizedSelector(uiSection) {
|
function createUnoptimizedSelector(uiSection) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createClientSideCollectionSelector('books', uiSection),
|
createBooksClientSideCollectionSelector(uiSection),
|
||||||
(books) => {
|
(books) => {
|
||||||
const items = books.items.map((s) => {
|
const items = books.items.map((s) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -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 _ from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
import filterCollection from 'Utilities/Array/filterCollection';
|
||||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
import sortCollection from 'Utilities/Array/sortCollection';
|
||||||
|
import createCustomFiltersSelector from './createCustomFiltersSelector';
|
||||||
function getSortClause(sortKey, sortDirection, sortPredicates) {
|
|
||||||
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
|
|
||||||
return function(item) {
|
|
||||||
return sortPredicates[sortKey](item, sortDirection);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return function(item) {
|
|
||||||
return item[sortKey];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function filter(items, state) {
|
|
||||||
const {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
filterPredicates
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
if (!selectedFilterKey) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
|
||||||
|
|
||||||
return _.filter(items, (item) => {
|
|
||||||
let i = 0;
|
|
||||||
let accepted = true;
|
|
||||||
|
|
||||||
while (accepted && i < selectedFilters.length) {
|
|
||||||
const {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
type = filterTypes.EQUAL
|
|
||||||
} = selectedFilters[i];
|
|
||||||
|
|
||||||
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
|
|
||||||
const predicate = filterPredicates[key];
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (
|
|
||||||
type === filterTypes.NOT_CONTAINS ||
|
|
||||||
type === filterTypes.NOT_EQUAL
|
|
||||||
) {
|
|
||||||
accepted = value.every((v) => predicate(item, v, type));
|
|
||||||
} else {
|
|
||||||
accepted = value.some((v) => predicate(item, v, type));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accepted = predicate(item, value, type);
|
|
||||||
}
|
|
||||||
} else if (item.hasOwnProperty(key)) {
|
|
||||||
const predicate = filterTypePredicates[type];
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (
|
|
||||||
type === filterTypes.NOT_CONTAINS ||
|
|
||||||
type === filterTypes.NOT_EQUAL
|
|
||||||
) {
|
|
||||||
accepted = value.every((v) => predicate(item[key], v));
|
|
||||||
} else {
|
|
||||||
accepted = value.some((v) => predicate(item[key], v));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accepted = predicate(item[key], value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default to false if the filter can't be tested
|
|
||||||
accepted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return accepted;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sort(items, state) {
|
|
||||||
const {
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
sortPredicates,
|
|
||||||
secondarySortKey,
|
|
||||||
secondarySortDirection
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const clauses = [];
|
|
||||||
const orders = [];
|
|
||||||
|
|
||||||
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
|
|
||||||
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
|
||||||
|
|
||||||
if (secondarySortKey &&
|
|
||||||
secondarySortDirection &&
|
|
||||||
(sortKey !== secondarySortKey ||
|
|
||||||
sortDirection !== secondarySortDirection)) {
|
|
||||||
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
|
|
||||||
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.orderBy(items, clauses, orders);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCustomFiltersSelector(type, alternateType) {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.customFilters.items,
|
|
||||||
(customFilters) => {
|
|
||||||
return customFilters.filter((customFilter) => {
|
|
||||||
return customFilter.type === type || customFilter.type === alternateType;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createClientSideCollectionSelector(section, uiSection) {
|
function createClientSideCollectionSelector(section, uiSection) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@@ -127,8 +12,8 @@ function createClientSideCollectionSelector(section, uiSection) {
|
|||||||
(sectionState, uiSectionState = {}, customFilters) => {
|
(sectionState, uiSectionState = {}, customFilters) => {
|
||||||
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
||||||
|
|
||||||
const filtered = filter(state.items, state);
|
const filtered = filterCollection(state.items, state);
|
||||||
const sorted = sort(filtered, state);
|
const sorted = sortCollection(filtered, state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sectionState,
|
...sectionState,
|
||||||
|
|||||||
@@ -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 {
|
@define-mixin scrollbar {
|
||||||
|
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
pageJumpBarZIndex: 10,
|
||||||
modalZIndex: 1000,
|
modalZIndex: 1000,
|
||||||
popperZIndex: 2000
|
popperZIndex: 2000
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ class About extends Component {
|
|||||||
packageVersion,
|
packageVersion,
|
||||||
packageAuthor,
|
packageAuthor,
|
||||||
isNetCore,
|
isNetCore,
|
||||||
isMono,
|
|
||||||
isDocker,
|
isDocker,
|
||||||
runtimeVersion,
|
runtimeVersion,
|
||||||
migrationVersion,
|
migrationVersion,
|
||||||
|
databaseVersion,
|
||||||
|
databaseType,
|
||||||
appData,
|
appData,
|
||||||
startupPath,
|
startupPath,
|
||||||
mode,
|
mode,
|
||||||
@@ -48,14 +49,6 @@ class About extends Component {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
isMono &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('MonoVersion')}
|
|
||||||
data={runtimeVersion}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
isNetCore &&
|
isNetCore &&
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -77,6 +70,11 @@ class About extends Component {
|
|||||||
data={migrationVersion}
|
data={migrationVersion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Database')}
|
||||||
|
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||||
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('AppDataDirectory')}
|
title={translate('AppDataDirectory')}
|
||||||
data={appData}
|
data={appData}
|
||||||
@@ -114,10 +112,11 @@ About.propTypes = {
|
|||||||
packageVersion: PropTypes.string,
|
packageVersion: PropTypes.string,
|
||||||
packageAuthor: PropTypes.string,
|
packageAuthor: PropTypes.string,
|
||||||
isNetCore: PropTypes.bool.isRequired,
|
isNetCore: PropTypes.bool.isRequired,
|
||||||
isMono: PropTypes.bool.isRequired,
|
|
||||||
runtimeVersion: PropTypes.string.isRequired,
|
runtimeVersion: PropTypes.string.isRequired,
|
||||||
isDocker: PropTypes.bool.isRequired,
|
isDocker: PropTypes.bool.isRequired,
|
||||||
migrationVersion: PropTypes.number.isRequired,
|
migrationVersion: PropTypes.number.isRequired,
|
||||||
|
databaseType: PropTypes.string.isRequired,
|
||||||
|
databaseVersion: PropTypes.string.isRequired,
|
||||||
appData: PropTypes.string.isRequired,
|
appData: PropTypes.string.isRequired,
|
||||||
startupPath: PropTypes.string.isRequired,
|
startupPath: PropTypes.string.isRequired,
|
||||||
mode: PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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() {
|
function getTranslations() {
|
||||||
let localization = null;
|
let localization = null;
|
||||||
const ajaxOptions = {
|
const ajaxOptions = {
|
||||||
async: false,
|
async: false,
|
||||||
type: 'GET',
|
|
||||||
global: false,
|
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
url: `${window.Readarr.apiRoot}/localization`,
|
url: '/localization',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
localization = data.Strings;
|
localization = data.Strings;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ajaxOptions.headers = ajaxOptions.headers || {};
|
createAjaxRequest(ajaxOptions);
|
||||||
ajaxOptions.headers['X-Api-Key'] = window.Readarr.apiKey;
|
|
||||||
|
|
||||||
$.ajax(ajaxOptions);
|
|
||||||
return localization;
|
return localization;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ export function isMobile() {
|
|||||||
export function isIOS() {
|
export function isIOS() {
|
||||||
return mobileDetect.is('iOS');
|
return mobileDetect.is('iOS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFirefox() {
|
||||||
|
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
|
||||||
|
}
|
||||||
+3
-2
@@ -33,7 +33,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "0.1.14",
|
"@fortawesome/react-fontawesome": "0.1.14",
|
||||||
"@microsoft/signalr": "6.0.5",
|
"@microsoft/signalr": "6.0.7",
|
||||||
"@sentry/browser": "6.18.2",
|
"@sentry/browser": "6.18.2",
|
||||||
"@sentry/integrations": "6.18.2",
|
"@sentry/integrations": "6.18.2",
|
||||||
"ansi-colors": "4.1.1",
|
"ansi-colors": "4.1.1",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"react-addons-shallow-compare": "15.6.3",
|
"react-addons-shallow-compare": "15.6.3",
|
||||||
"react-async-script": "1.2.0",
|
"react-async-script": "1.2.0",
|
||||||
"react-autosuggest": "10.1.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": "14.0.2",
|
||||||
"react-dnd-html5-backend": "14.0.0",
|
"react-dnd-html5-backend": "14.0.0",
|
||||||
"react-dnd-multi-backend": "6.0.2",
|
"react-dnd-multi-backend": "6.0.2",
|
||||||
@@ -109,6 +109,7 @@
|
|||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.23.4",
|
"eslint-plugin-import": "2.23.4",
|
||||||
"eslint-plugin-react": "7.24.0",
|
"eslint-plugin-react": "7.24.0",
|
||||||
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||||
"esprint": "3.1.0",
|
"esprint": "3.1.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
|
|||||||
@@ -4,18 +4,19 @@
|
|||||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||||
<PackageVersion Include="Dapper" Version="2.0.123" />
|
<PackageVersion Include="Dapper" Version="2.0.123" />
|
||||||
<PackageVersion Include="DryIoc.dll" Version="4.8.7" />
|
<PackageVersion Include="DryIoc.dll" Version="5.2.0" />
|
||||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
|
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" />
|
||||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" />
|
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||||
<PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" />
|
<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="FluentValidation" Version="8.6.2" />
|
||||||
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
||||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||||
<PackageVersion Include="Mailkit" Version="3.1.1" />
|
<PackageVersion Include="Mailkit" Version="3.3.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.5" />
|
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
@@ -24,41 +25,41 @@
|
|||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||||
<PackageVersion Include="MonoTorrent" Version="2.0.5" />
|
|
||||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||||
|
<PackageVersion Include="MonoTorrent" Version="2.0.6" />
|
||||||
<PackageVersion Include="NBuilder" Version="6.1.0" />
|
<PackageVersion Include="NBuilder" Version="6.1.0" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
|
<PackageVersion Include="NLog.Extensions.Logging" Version="5.1.0" />
|
||||||
<PackageVersion Include="NLog" Version="4.7.14" />
|
<PackageVersion Include="NLog" Version="5.0.5" />
|
||||||
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
|
<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="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="NunitXml.TestLogger" Version="3.0.117" />
|
||||||
<PackageVersion Include="PdfSharpCore" Version="1.3.18" />
|
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||||
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
||||||
<PackageVersion Include="Sentry" Version="3.14.1" />
|
<PackageVersion Include="Sentry" Version="3.20.1" />
|
||||||
<PackageVersion Include="SharpZipLib" Version="1.3.3" />
|
<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="StyleCop.Analyzers" Version="1.1.118" />
|
||||||
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
||||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="16.1.20" />
|
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.Abstractions" Version="16.1.20" />
|
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageVersion Include="System.Memory" Version="4.5.4" />
|
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||||
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="6.0.4" />
|
<PackageVersion Include="System.Text.Json" Version="6.0.5" />
|
||||||
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||||
<PackageVersion Include="Unity" Version="5.11.10" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
+5
-6
@@ -1,13 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
|
|
||||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||||
|
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
<packageSourceMapping>
|
<packageSourceMapping>
|
||||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||||
@@ -17,10 +17,6 @@
|
|||||||
<packageSource key="dotnet-bsd-crossbuild">
|
<packageSource key="dotnet-bsd-crossbuild">
|
||||||
<package pattern="*" />
|
<package pattern="*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
<packageSource key="FluentMigrator">
|
|
||||||
<package pattern="FluentMigrator" />
|
|
||||||
<package pattern="FluentMigrator.*" />
|
|
||||||
</packageSource>
|
|
||||||
<packageSource key="Mono.Posix.NETStandard">
|
<packageSource key="Mono.Posix.NETStandard">
|
||||||
<package pattern="Mono.Posix.NETStandard" />
|
<package pattern="Mono.Posix.NETStandard" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
@@ -30,5 +26,8 @@
|
|||||||
<packageSource key="coverlet-nightly">
|
<packageSource key="coverlet-nightly">
|
||||||
<package pattern="coverlet.*" />
|
<package pattern="coverlet.*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
|
<packageSource key="FluentMigrator">
|
||||||
|
<package pattern="Servarr.FluentMigrator*"/>
|
||||||
|
</packageSource>
|
||||||
</packageSourceMapping>
|
</packageSourceMapping>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
|
|||||||
|
|
||||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start();
|
_runner.Start();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
||||||
where TSubject : class, IDiskProvider
|
where TSubject : class, IDiskProvider
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void BaseSetup()
|
||||||
|
{
|
||||||
|
Mocker.SetConstant<IFileSystem>(new FileSystem());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void writealltext_should_truncate_existing()
|
public void writealltext_should_truncate_existing()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -437,24 +437,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
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]
|
[Test]
|
||||||
public void CopyFolder_should_ignore_nfs_temp_file()
|
public void CopyFolder_should_ignore_nfs_temp_file()
|
||||||
{
|
{
|
||||||
@@ -540,26 +522,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
source.FullName.GetActualCasing().Should().Be(destination.FullName);
|
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]
|
[Test]
|
||||||
public void should_throw_if_destination_is_readonly()
|
public void should_throw_if_destination_is_readonly()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||||||
public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject>
|
public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject>
|
||||||
where TSubject : class, IDiskProvider
|
where TSubject : class, IDiskProvider
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void BaseSetup()
|
||||||
|
{
|
||||||
|
Mocker.SetConstant<IFileSystem>(new FileSystem());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_get_free_space_for_folder()
|
public void should_get_free_space_for_folder()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||||
public void should_execute_get_using_brotli()
|
public void should_execute_get_using_brotli()
|
||||||
{
|
{
|
||||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||||
@@ -307,11 +308,6 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_follow_redirects_to_https()
|
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")
|
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
|
||||||
.AddQueryParam("url", $"https://readarr.com/")
|
.AddQueryParam("url", $"https://readarr.com/")
|
||||||
.Build();
|
.Build();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Instrumentation;
|
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("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||||
|
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||||
|
[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
|
// Announce URLs (passkeys) Magnet & Tracker
|
||||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||||
@@ -70,14 +72,38 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
|
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||||
|
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||||
|
|
||||||
|
// 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)
|
public void should_clean_message(string message)
|
||||||
{
|
{
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|
||||||
cleansedMessage.Should().NotContain("mySecret");
|
cleansedMessage.Should().NotContain("mySecret");
|
||||||
|
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||||
cleansedMessage.Should().NotContain("01233210");
|
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
|
//GoodReads
|
||||||
[TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")]
|
[TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")]
|
||||||
[TestCase(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
|
[TestCase(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ namespace NzbDrone.Common.Test
|
|||||||
[Test]
|
[Test]
|
||||||
public void GetUpdateClientExePath()
|
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]
|
[Test]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -171,7 +171,7 @@ namespace NzbDrone.Common.Test
|
|||||||
var processStarted = new ManualResetEventSlim();
|
var processStarted = new ManualResetEventSlim();
|
||||||
|
|
||||||
string suffix;
|
string suffix;
|
||||||
if (OsInfo.IsWindows || PlatformInfo.IsMono)
|
if (OsInfo.IsWindows)
|
||||||
{
|
{
|
||||||
suffix = ".exe";
|
suffix = ".exe";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Composition.Extensions;
|
using NzbDrone.Common.Composition.Extensions;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Extensions;
|
using NzbDrone.Core.Datastore.Extensions;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
|
|||||||
.AddDummyDatabase()
|
.AddDummyDatabase()
|
||||||
.AddStartupContext(new StartupContext("first", "second"));
|
.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();
|
var serviceProvider = container.GetServiceProvider();
|
||||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
|||||||
"/boot",
|
"/boot",
|
||||||
"/lib",
|
"/lib",
|
||||||
"/sbin",
|
"/sbin",
|
||||||
"/proc"
|
"/proc",
|
||||||
|
"/usr/bin"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.EnvironmentInfo
|
namespace NzbDrone.Common.EnvironmentInfo
|
||||||
{
|
{
|
||||||
public enum PlatformType
|
|
||||||
{
|
|
||||||
DotNet = 0,
|
|
||||||
Mono = 1,
|
|
||||||
NetCore = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPlatformInfo
|
public interface IPlatformInfo
|
||||||
{
|
{
|
||||||
Version Version { get; }
|
Version Version { get; }
|
||||||
@@ -19,38 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
|
|
||||||
public class PlatformInfo : IPlatformInfo
|
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;
|
private static Version _version;
|
||||||
|
|
||||||
static PlatformInfo()
|
static PlatformInfo()
|
||||||
{
|
{
|
||||||
_platform = PlatformType.NetCore;
|
|
||||||
_version = Environment.Version;
|
_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
|
public static string PlatformName
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (IsDotNet)
|
return ".NET";
|
||||||
{
|
|
||||||
return ".NET";
|
|
||||||
}
|
|
||||||
else if (IsMono)
|
|
||||||
{
|
|
||||||
return "Mono";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ".NET Core";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,107 +30,5 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
{
|
{
|
||||||
return _version;
|
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;
|
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";
|
processName += ".exe";
|
||||||
}
|
}
|
||||||
@@ -250,11 +250,6 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return processName;
|
return processName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ProcessNameToExe(this string processName)
|
|
||||||
{
|
|
||||||
return processName.ProcessNameToExe(PlatformInfo.Platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetLongestCommonPath(this List<string> paths)
|
public static string GetLongestCommonPath(this List<string> paths)
|
||||||
{
|
{
|
||||||
var firstPath = paths.First();
|
var firstPath = paths.First();
|
||||||
@@ -347,9 +342,9 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
|
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)
|
public static string GetDatabase(this IAppFolderInfo appFolderInfo)
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Common.Http
|
|||||||
StatusCode == HttpStatusCode.Found ||
|
StatusCode == HttpStatusCode.Found ||
|
||||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||||
StatusCode == HttpStatusCode.SeeOther;
|
StatusCode == HttpStatusCode.SeeOther ||
|
||||||
|
StatusCode == HttpStatusCode.PermanentRedirect;
|
||||||
|
|
||||||
public string[] GetCookieHeaders()
|
public string[] GetCookieHeaders()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -11,13 +11,14 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
private static readonly Regex[] CleansingRules = new[]
|
private static readonly Regex[] CleansingRules = new[]
|
||||||
{
|
{
|
||||||
// Url
|
// Url
|
||||||
new Regex(@"(?<=\?|&|: )((?:api|auth|pass)?key|(?:access[-_]?)?token|auth|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?<=\?|&|: )((?: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(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"\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
|
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||||
@@ -46,7 +47,14 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Good Reads
|
// Good Reads
|
||||||
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
// Notifiarr
|
||||||
|
new Regex(@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
|
// 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);
|
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;
|
||||||
using NLog.Fluent;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||||
{
|
{
|
||||||
@@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
|||||||
{
|
{
|
||||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
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);
|
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);
|
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);
|
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);
|
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);
|
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)
|
SentryLogger.ForLogEvent(level)
|
||||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
.CopyLogEvent(logBuilder.LogEvent)
|
||||||
.SentryFingerprint(fingerprint)
|
.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)
|
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||||
.TimeStamp(logEvent.TimeStamp)
|
|
||||||
.Message(logEvent.Message, logEvent.Parameters)
|
.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);
|
.Exception(logEvent.Exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,16 +38,6 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
return;
|
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);
|
Console.WriteLine("EPIC FAIL: {0}", exception);
|
||||||
Logger.Fatal(exception, "EPIC FAIL.");
|
Logger.Fatal(exception, "EPIC FAIL.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
using NLog;
|
using System.Text;
|
||||||
|
using NLog;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation
|
namespace NzbDrone.Common.Instrumentation
|
||||||
{
|
{
|
||||||
public class NzbDroneFileTarget : FileTarget
|
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);
|
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||||
|
|
||||||
|
RegisterGlobalFilters();
|
||||||
|
|
||||||
if (Debugger.IsAttached)
|
if (Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
@@ -101,6 +103,16 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
LogManager.Configuration.LoggingRules.Add(loggingRule);
|
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()
|
private static void RegisterConsole()
|
||||||
{
|
{
|
||||||
var level = LogLevel.Trace;
|
var level = LogLevel.Trace;
|
||||||
|
|||||||
@@ -109,13 +109,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.Debug = false;
|
o.Debug = false;
|
||||||
o.DiagnosticLevel = SentryLevel.Debug;
|
o.DiagnosticLevel = SentryLevel.Debug;
|
||||||
o.Release = BuildInfo.Release;
|
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.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
@@ -158,14 +151,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
|
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)
|
if (ex != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.GetType().FullName);
|
fingerPrint.Add(ex.GetType().FullName);
|
||||||
fingerPrint.Add(ex.TargetSite.ToString());
|
if (ex.TargetSite != null)
|
||||||
|
{
|
||||||
|
fingerPrint.Add(ex.TargetSite.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||||
|
|||||||
@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
|
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
|
||||||
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
|
|
||||||
|
var key = environmentVariable.Key.ToString();
|
||||||
|
var value = environmentVariable.Value?.ToString();
|
||||||
|
|
||||||
|
if (startInfo.EnvironmentVariables.ContainsKey(key))
|
||||||
|
{
|
||||||
|
startInfo.EnvironmentVariables[key] = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startInfo.EnvironmentVariables.Add(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -366,11 +377,6 @@ namespace NzbDrone.Common.Processes
|
|||||||
|
|
||||||
private (string Path, string Args) GetPathAndArgs(string path, string args)
|
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))
|
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return ("cmd.exe", $"/c {path} {args}");
|
return ("cmd.exe", $"/c {path} {args}");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
public void SingleOrDefault_should_return_null_on_empty_db()
|
public void SingleOrDefault_should_return_null_on_empty_db()
|
||||||
{
|
{
|
||||||
Mocker.Resolve<IDatabase>()
|
Mocker.Resolve<IDatabase>()
|
||||||
.OpenConnection().Query<Author>("SELECT * FROM Authors")
|
.OpenConnection().Query<Author>("SELECT * FROM \"Authors\"")
|
||||||
.SingleOrDefault(c => c.CleanName == "SomeTitle")
|
.SingleOrDefault(c => c.CleanName == "SomeTitle")
|
||||||
.Should()
|
.Should()
|
||||||
.BeNull();
|
.BeNull();
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
public void should_lazy_load_author_for_trackfile()
|
public void should_lazy_load_author_for_trackfile()
|
||||||
{
|
{
|
||||||
var db = Mocker.Resolve<IDatabase>();
|
var db = Mocker.Resolve<IDatabase>();
|
||||||
var tracks = db.Query<BookFile>(new SqlBuilder()).ToList();
|
var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||||
|
|
||||||
Assert.IsNotEmpty(tracks);
|
Assert.IsNotEmpty(tracks);
|
||||||
foreach (var track in tracks)
|
foreach (var track in tracks)
|
||||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
public void should_lazy_load_trackfile_if_not_joined()
|
public void should_lazy_load_trackfile_if_not_joined()
|
||||||
{
|
{
|
||||||
var db = Mocker.Resolve<IDatabase>();
|
var db = Mocker.Resolve<IDatabase>();
|
||||||
var tracks = db.Query<Book>(new SqlBuilder()).ToList();
|
var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||||
|
|
||||||
foreach (var track in tracks)
|
foreach (var track in tracks)
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
{
|
{
|
||||||
var db = Mocker.Resolve<IDatabase>();
|
var db = Mocker.Resolve<IDatabase>();
|
||||||
var files = MediaFileRepository.Query(db,
|
var files = MediaFileRepository.Query(db,
|
||||||
new SqlBuilder()
|
new SqlBuilder(db.DatabaseType)
|
||||||
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
|
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
|
||||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||||
|
|||||||
@@ -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))");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
|
|||||||
namespace NzbDrone.Core.Test.Datastore
|
namespace NzbDrone.Core.Test.Datastore
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class WhereBuilderFixture : CoreTest
|
public class WhereBuilderSqliteFixture : CoreTest
|
||||||
{
|
{
|
||||||
private WhereBuilder _subject;
|
private WhereBuilderSqlite _subject;
|
||||||
|
|
||||||
[OneTimeSetUp]
|
[OneTimeSetUp]
|
||||||
public void MapTables()
|
public void MapTables()
|
||||||
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
Mocker.Resolve<DbFactory>();
|
Mocker.Resolve<DbFactory>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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]
|
[Test]
|
||||||
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||||
{
|
{
|
||||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||||
_subject = new WhereBuilder(filter, true, 0);
|
_subject = new WhereBuilderSqlite(filter, true, 0);
|
||||||
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||||||
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||||
{
|
{
|
||||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||||
_subject = new WhereBuilder(filter, false, 0);
|
_subject = new WhereBuilderSqlite(filter, false, 0);
|
||||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
@@ -5,6 +6,7 @@ using FluentAssertions;
|
|||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -244,5 +246,89 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
.Should()
|
.Should()
|
||||||
.BeFalse();
|
.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.Collections.Generic;
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Npgsql;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
using NzbDrone.Test.Common.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Framework
|
namespace NzbDrone.Core.Test.Framework
|
||||||
{
|
{
|
||||||
@@ -47,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
public abstract class DbTest : CoreTest
|
public abstract class DbTest : CoreTest
|
||||||
{
|
{
|
||||||
private ITestDatabase _db;
|
private ITestDatabase _db;
|
||||||
|
private DatabaseType _databaseType;
|
||||||
|
|
||||||
protected virtual MigrationType MigrationType => MigrationType.Main;
|
protected virtual MigrationType MigrationType => MigrationType.Main;
|
||||||
|
|
||||||
@@ -65,8 +72,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
|
|
||||||
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
||||||
{
|
{
|
||||||
var factory = Mocker.Resolve<DbFactory>();
|
var database = CreateDatabase(migrationContext);
|
||||||
var database = factory.Create(migrationContext);
|
|
||||||
Mocker.SetConstant(database);
|
Mocker.SetConstant(database);
|
||||||
|
|
||||||
switch (MigrationType)
|
switch (MigrationType)
|
||||||
@@ -98,6 +104,65 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
return testDb;
|
return testDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IDatabase CreateDatabase(MigrationContext migrationContext)
|
||||||
|
{
|
||||||
|
if (_databaseType == DatabaseType.PostgreSQL)
|
||||||
|
{
|
||||||
|
CreatePostgresDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
var factory = Mocker.Resolve<DbFactory>();
|
||||||
|
|
||||||
|
// If a special migration test or log migration then create new
|
||||||
|
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
|
||||||
|
{
|
||||||
|
return factory.Create(migrationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateSqliteDatabase(factory, migrationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreatePostgresDb()
|
||||||
|
{
|
||||||
|
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
|
||||||
|
PostgresDatabase.Create(options, MigrationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropPostgresDb()
|
||||||
|
{
|
||||||
|
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
|
||||||
|
PostgresDatabase.Drop(options, MigrationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
|
||||||
|
{
|
||||||
|
// Otherwise try to use a cached migrated db
|
||||||
|
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
|
||||||
|
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
|
||||||
|
if (File.Exists(cachedDb))
|
||||||
|
{
|
||||||
|
TestLogger.Info($"Using cached initial database {cachedDb}");
|
||||||
|
File.Copy(cachedDb, testDb);
|
||||||
|
return factory.Create(migrationContext);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var db = factory.Create(migrationContext);
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
SQLiteConnection.ClearAllPools();
|
||||||
|
|
||||||
|
TestLogger.Info("Caching database");
|
||||||
|
File.Copy(testDb, cachedDb);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTestSqliteDb(MigrationType type)
|
||||||
|
{
|
||||||
|
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void SetupLogging()
|
protected virtual void SetupLogging()
|
||||||
{
|
{
|
||||||
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
|
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
|
||||||
@@ -108,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
WithTempAsAppPath();
|
WithTempAsAppPath();
|
||||||
SetupLogging();
|
SetupLogging();
|
||||||
|
|
||||||
|
// populate the possible postgres options
|
||||||
|
var postgresOptions = PostgresDatabase.GetTestOptions();
|
||||||
|
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
|
||||||
|
|
||||||
|
// Set up remaining container services
|
||||||
|
Mocker.SetConstant(Options.Create(postgresOptions));
|
||||||
|
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
|
||||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||||
|
|
||||||
@@ -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)
|
// Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
GC.WaitForPendingFinalizers();
|
GC.WaitForPendingFinalizers();
|
||||||
|
|
||||||
SQLiteConnection.ClearAllPools();
|
SQLiteConnection.ClearAllPools();
|
||||||
|
NpgsqlConnection.ClearAllPools();
|
||||||
|
|
||||||
if (TestFolderInfo != null)
|
if (TestFolderInfo != null)
|
||||||
{
|
{
|
||||||
DeleteTempFolder(TestFolderInfo.AppDataFolder);
|
DeleteTempFolder(TestFolderInfo.AppDataFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_databaseType == DatabaseType.PostgreSQL)
|
||||||
|
{
|
||||||
|
DropPostgresDb();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using Unity.Resolution;
|
using NzbDrone.Test.Common.AutoMoq;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Framework
|
namespace NzbDrone.Core.Test.Framework
|
||||||
{
|
{
|
||||||
@@ -14,12 +15,9 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void FileSystemTestSetup()
|
public void FileSystemTestSetup()
|
||||||
{
|
{
|
||||||
FileSystem = new MockFileSystem();
|
FileSystem = (MockFileSystem)Mocker.Resolve<IFileSystem>(FileSystemType.Mock);
|
||||||
|
|
||||||
DiskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider", new ResolverOverride[]
|
DiskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Mock);
|
||||||
{
|
|
||||||
new ParameterOverride("fileSystem", FileSystem)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
where T : ModelBase, new();
|
where T : ModelBase, new();
|
||||||
IDirectDataMapper GetDirectDataMapper();
|
IDirectDataMapper GetDirectDataMapper();
|
||||||
IDbConnection OpenConnection();
|
IDbConnection OpenConnection();
|
||||||
|
DatabaseType DatabaseType { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestDatabase : ITestDatabase
|
public class TestDatabase : ITestDatabase
|
||||||
@@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
private readonly IDatabase _dbConnection;
|
private readonly IDatabase _dbConnection;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
|
||||||
|
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
|
||||||
|
|
||||||
public TestDatabase(IDatabase dbConnection)
|
public TestDatabase(IDatabase dbConnection)
|
||||||
{
|
{
|
||||||
_eventAggregator = new Mock<IEventAggregator>().Object;
|
_eventAggregator = new Mock<IEventAggregator>().Object;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
|||||||
.Setup(s => s.GetAuthor(It.IsAny<int>()))
|
.Setup(s => s.GetAuthor(It.IsAny<int>()))
|
||||||
.Returns(_author);
|
.Returns(_author);
|
||||||
|
|
||||||
Mocker.GetMock<ISearchForNzb>()
|
Mocker.GetMock<ISearchForReleases>()
|
||||||
.Setup(s => s.AuthorSearch(_author.Id, false, true, false))
|
.Setup(s => s.AuthorSearch(_author.Id, false, true, false))
|
||||||
.Returns(new List<DownloadDecision>());
|
.Returns(new List<DownloadDecision>());
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
|||||||
|
|
||||||
Subject.Execute(new AuthorSearchCommand { AuthorId = _author.Id, Trigger = CommandTrigger.Manual });
|
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),
|
.Verify(v => v.AuthorSearch(_author.Id, false, true, false),
|
||||||
Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
|
Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
if (PlatformInfo.IsMono && PlatformInfo.GetVersion() < new Version(5, 8))
|
|
||||||
{
|
|
||||||
Assert.Inconclusive("Not supported on Mono < 5.8");
|
|
||||||
}
|
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.FileExists(It.IsAny<string>()))
|
.Setup(v => v.FileExists(It.IsAny<string>()))
|
||||||
.Returns<string>(s => File.Exists(s));
|
.Returns<string>(s => File.Exists(s));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -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);
|
Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers);
|
||||||
|
|
||||||
covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension);
|
covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using NzbDrone.Core.Configuration;
|
|||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Test.Common.AutoMoq;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||||
{
|
{
|
||||||
@@ -55,9 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider");
|
_diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual);
|
||||||
|
|
||||||
Mocker.SetConstant<IDiskProvider>(_diskProvider);
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>()
|
Mocker.GetMock<IConfigService>()
|
||||||
.Setup(x => x.WriteAudioTags)
|
.Setup(x => x.WriteAudioTags)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
|||||||
[Test]
|
[Test]
|
||||||
public void getting_details_of_invalid_book()
|
public void getting_details_of_invalid_book()
|
||||||
{
|
{
|
||||||
Assert.Throws<BookNotFoundException>(() => Subject.GetBookInfo("99999999"));
|
Assert.Throws<BookNotFoundException>(() => Subject.GetBookInfo("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateAuthor(Author author)
|
private void ValidateAuthor(Author author)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Harry Potter and the sorcerer's stone", 3)]
|
[TestCase("Harry Potter and the sorcerer's stone", 3)]
|
||||||
[TestCase("B0192CTMYG", 42844155)]
|
[TestCase("B0192CTMYG", 61209488)]
|
||||||
[TestCase("9780439554930", 48517161)]
|
[TestCase("9780439554930", 48517161)]
|
||||||
public void successful_book_search(string title, int expected)
|
public void successful_book_search(string title, int expected)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using FluentAssertions.Equivalency;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||||
@@ -21,6 +23,13 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
|
AssertionOptions.AssertEquivalencyUsing(options =>
|
||||||
|
{
|
||||||
|
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
|
||||||
|
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
_author = new Author
|
_author = new Author
|
||||||
{
|
{
|
||||||
Name = "Alien Ant Farm",
|
Name = "Alien Ant Farm",
|
||||||
@@ -143,7 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
|||||||
GivenMultipleBooks();
|
GivenMultipleBooks();
|
||||||
|
|
||||||
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
|
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
|
||||||
result.Should().BeEquivalentTo(_books.Take(1));
|
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -152,7 +161,11 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
|||||||
GivenMultipleBooks();
|
GivenMultipleBooks();
|
||||||
|
|
||||||
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
|
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
|
||||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1));
|
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 System.Data.SQLite;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Npgsql;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Books;
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Profiles.Metadata;
|
using NzbDrone.Core.Profiles.Metadata;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
@@ -145,7 +147,14 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
|
|||||||
_authorRepo.Insert(author1);
|
_authorRepo.Insert(author1);
|
||||||
|
|
||||||
Action insertDupe = () => _authorRepo.Insert(author2);
|
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)
|
private void GivenBooksForRefresh(List<Book> books)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||||
.Returns(books);
|
.Returns(books);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
|
|
||||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||||
.InSequence(seq)
|
.InSequence(seq)
|
||||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||||
.Returns(new List<Book>());
|
.Returns(new List<Book>());
|
||||||
|
|
||||||
// Update called twice for a move/merge
|
// Update called twice for a move/merge
|
||||||
@@ -298,7 +298,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
|
|
||||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||||
.InSequence(seq)
|
.InSequence(seq)
|
||||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<IEnumerable<string>>()))
|
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>()))
|
||||||
.Returns(_books);
|
.Returns(_books);
|
||||||
|
|
||||||
// Update called twice for a move/merge
|
// Update called twice for a move/merge
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using NzbDrone.Core.Test.Framework;
|
|||||||
namespace NzbDrone.Core.Test.ParserTests
|
namespace NzbDrone.Core.Test.ParserTests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
|
|
||||||
public class QualityParserFixture : CoreTest
|
public class QualityParserFixture : CoreTest
|
||||||
{
|
{
|
||||||
public static object[] SelfQualityParserCases =
|
public static object[] SelfQualityParserCases =
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
|||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
|
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
|
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
|
||||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
|||||||
public void should_return_with_warning_if_updater_doesnt_exists()
|
public void should_return_with_warning_if_updater_doesnt_exists()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
|
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
Subject.Execute(new ApplicationUpdateCommand());
|
Subject.Execute(new ApplicationUpdateCommand());
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
|
|||||||
|
|
||||||
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
|
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
|
||||||
{
|
{
|
||||||
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||||
|
|
||||||
private readonly IMainDatabase _database;
|
private readonly IMainDatabase _database;
|
||||||
|
|
||||||
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqlBuilder Builder() => new SqlBuilder()
|
private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||||
.Select(@"Authors.Id AS AuthorId,
|
.Select(@"""Authors"".""Id"" AS ""AuthorId"",
|
||||||
Books.Id AS BookId,
|
""Books"".""Id"" AS ""BookId"",
|
||||||
SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk,
|
SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"",
|
||||||
1 AS TotalBookCount,
|
1 AS ""TotalBookCount"",
|
||||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount,
|
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE 1 END AS ""AvailableBookCount"",
|
||||||
CASE WHEN (Books.Monitored = 1 AND (Books.ReleaseDate < @currentDate) OR Books.ReleaseDate IS NULL) OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END AS BookCount,
|
CASE WHEN (""Books"".""Monitored"" = true AND (""Books"".""ReleaseDate"" < @currentDate) OR ""Books"".""ReleaseDate"" IS NULL) OR MIN(""BookFiles"".""Id"") IS NOT NULL THEN 1 ELSE 0 END AS ""BookCount"",
|
||||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount")
|
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""")
|
||||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||||
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
|
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ namespace NzbDrone.Core.AuthorStats
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class AuthorStatisticsService : IAuthorStatisticsService,
|
public class AuthorStatisticsService : IAuthorStatisticsService,
|
||||||
|
IHandle<AuthorAddedEvent>,
|
||||||
IHandle<AuthorUpdatedEvent>,
|
IHandle<AuthorUpdatedEvent>,
|
||||||
IHandle<AuthorDeletedEvent>,
|
IHandle<AuthorDeletedEvent>,
|
||||||
IHandle<BookAddedEvent>,
|
IHandle<BookAddedEvent>,
|
||||||
IHandle<BookDeletedEvent>,
|
IHandle<BookDeletedEvent>,
|
||||||
IHandle<BookImportedEvent>,
|
IHandle<BookImportedEvent>,
|
||||||
IHandle<BookEditedEvent>,
|
IHandle<BookEditedEvent>,
|
||||||
|
IHandle<BookUpdatedEvent>,
|
||||||
IHandle<BookFileDeletedEvent>
|
IHandle<BookFileDeletedEvent>
|
||||||
{
|
{
|
||||||
private readonly IAuthorStatisticsRepository _authorStatisticsRepository;
|
private readonly IAuthorStatisticsRepository _authorStatisticsRepository;
|
||||||
@@ -68,6 +70,13 @@ namespace NzbDrone.Core.AuthorStats
|
|||||||
return authorStatistics;
|
return authorStatistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[EventHandleOrder(EventHandleOrder.First)]
|
||||||
|
public void Handle(AuthorAddedEvent message)
|
||||||
|
{
|
||||||
|
_cache.Remove("AllAuthors");
|
||||||
|
_cache.Remove(message.Author.Id.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[EventHandleOrder(EventHandleOrder.First)]
|
[EventHandleOrder(EventHandleOrder.First)]
|
||||||
public void Handle(AuthorUpdatedEvent message)
|
public void Handle(AuthorUpdatedEvent message)
|
||||||
{
|
{
|
||||||
@@ -110,6 +119,13 @@ namespace NzbDrone.Core.AuthorStats
|
|||||||
_cache.Remove(message.Book.AuthorId.ToString());
|
_cache.Remove(message.Book.AuthorId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[EventHandleOrder(EventHandleOrder.First)]
|
||||||
|
public void Handle(BookUpdatedEvent message)
|
||||||
|
{
|
||||||
|
_cache.Remove("AllAuthors");
|
||||||
|
_cache.Remove(message.Book.AuthorId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[EventHandleOrder(EventHandleOrder.First)]
|
[EventHandleOrder(EventHandleOrder.First)]
|
||||||
public void Handle(BookFileDeletedEvent message)
|
public void Handle(BookFileDeletedEvent message)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -183,9 +183,12 @@ namespace NzbDrone.Core.Backup
|
|||||||
|
|
||||||
private void BackupDatabase()
|
private void BackupDatabase()
|
||||||
{
|
{
|
||||||
_logger.ProgressDebug("Backing up database");
|
if (_maindDb.DatabaseType == DatabaseType.SQLite)
|
||||||
|
{
|
||||||
|
_logger.ProgressDebug("Backing up database");
|
||||||
|
|
||||||
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
|
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackupConfigFile()
|
private void BackupConfigFile()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
return Query(b => b.AuthorId == authorId);
|
return Query(b => b.AuthorId == authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder()
|
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||||
.Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id)
|
.Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id)
|
||||||
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
|
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
|
||||||
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder,
|
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SqlBuilder Builder() => new SqlBuilder()
|
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||||
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id);
|
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id);
|
||||||
|
|
||||||
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
|
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
|
||||||
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
using (var conn = _database.OpenConnection())
|
using (var conn = _database.OpenConnection())
|
||||||
{
|
{
|
||||||
var strSql = "SELECT Id AS [Key], Path AS [Value] FROM Authors";
|
var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Authors\"";
|
||||||
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
|
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Books
|
|||||||
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
|
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
|
||||||
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
|
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
|
||||||
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
||||||
List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
|
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
|
||||||
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
||||||
Book FindByTitle(int authorMetadataId, string title);
|
Book FindByTitle(int authorMetadataId, string title);
|
||||||
Book FindById(string foreignBookId);
|
Book FindById(string foreignBookId);
|
||||||
@@ -44,17 +44,35 @@ namespace NzbDrone.Core.Books
|
|||||||
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
|
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
|
|
||||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
var inner = Builder()
|
||||||
.Having("Books.ReleaseDate = MAX(Books.ReleaseDate)"));
|
.Select("MIN(\"Books\".\"Id\") as id, MAX(\"Books\".\"ReleaseDate\") as date")
|
||||||
|
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
|
||||||
|
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||||
|
.AddSelectTemplate(typeof(Book));
|
||||||
|
|
||||||
|
var outer = Builder()
|
||||||
|
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
|
||||||
|
.AddParameters(inner.Parameters);
|
||||||
|
|
||||||
|
return Query(outer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds)
|
public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
|
|
||||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
var inner = Builder()
|
||||||
.Having("Books.ReleaseDate = MIN(Books.ReleaseDate)"));
|
.Select("MIN(\"Books\".\"Id\") as id, MIN(\"Books\".\"ReleaseDate\") as date")
|
||||||
|
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
|
||||||
|
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||||
|
.AddSelectTemplate(typeof(Book));
|
||||||
|
|
||||||
|
var outer = Builder()
|
||||||
|
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
|
||||||
|
.AddParameters(inner.Parameters);
|
||||||
|
|
||||||
|
return Query(outer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
||||||
@@ -62,14 +80,14 @@ namespace NzbDrone.Core.Books
|
|||||||
return Query(s => s.AuthorMetadataId == authorMetadataId);
|
return Query(s => s.AuthorMetadataId == authorMetadataId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
|
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
|
||||||
{
|
{
|
||||||
return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId));
|
return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
|
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
|
||||||
{
|
{
|
||||||
return Query(new SqlBuilder()
|
return Query(new SqlBuilder(_database.DatabaseType)
|
||||||
.Join<Book, Edition>((b, e) => b.Id == e.BookId)
|
.Join<Book, Edition>((b, e) => b.Id == e.BookId)
|
||||||
.Join<Edition, BookFile>((l, r) => l.Id == r.EditionId)
|
.Join<Edition, BookFile>((l, r) => l.Id == r.EditionId)
|
||||||
.Where<BookFile>(f => fileIds.Contains(f.Id)))
|
.Where<BookFile>(f => fileIds.Contains(f.Id)))
|
||||||
@@ -125,7 +143,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
foreach (var belowCutoff in profile.QualityIds)
|
foreach (var belowCutoff in profile.QualityIds)
|
||||||
{
|
{
|
||||||
clauses.Add(string.Format("(Authors.[QualityProfileId] = {0} AND BookFiles.Quality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
|
clauses.Add(string.Format("(\"Authors\".\"QualityProfileId\" = {0} AND \"BookFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +154,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
|
pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
|
||||||
|
|
||||||
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Book))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)";
|
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Book))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
|
||||||
pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate);
|
pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate);
|
||||||
|
|
||||||
return pagingSpec;
|
return pagingSpec;
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
List<Edition> GetAllMonitoredEditions();
|
List<Edition> GetAllMonitoredEditions();
|
||||||
Edition FindByForeignEditionId(string foreignEditionId);
|
Edition FindByForeignEditionId(string foreignEditionId);
|
||||||
List<Edition> FindByBook(int id);
|
List<Edition> FindByBook(IEnumerable<int> ids);
|
||||||
List<Edition> FindByAuthor(int id);
|
List<Edition> FindByAuthor(int id);
|
||||||
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
|
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
|
||||||
Edition FindByTitle(int authorMetadataId, string title);
|
Edition FindByTitle(int authorMetadataId, string title);
|
||||||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds);
|
||||||
List<Edition> SetMonitored(Edition edition);
|
List<Edition> SetMonitored(Edition edition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,19 +38,19 @@ namespace NzbDrone.Core.Books
|
|||||||
return edition;
|
return edition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
|
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds)
|
||||||
{
|
{
|
||||||
return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId));
|
return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Edition> FindByBook(int id)
|
public List<Edition> FindByBook(IEnumerable<int> ids)
|
||||||
{
|
{
|
||||||
// populate the books and author metadata also
|
// populate the books and author metadata also
|
||||||
// this hopefully speeds up the track matching a lot
|
// this hopefully speeds up the track matching a lot
|
||||||
var builder = new SqlBuilder()
|
var builder = new SqlBuilder(_database.DatabaseType)
|
||||||
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
|
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||||
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
|
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
|
||||||
.Where<Edition>(r => r.BookId == id);
|
.Where<Edition>(r => ids.Contains(r.BookId));
|
||||||
|
|
||||||
return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) =>
|
return _database.QueryJoined<Edition, Book, AuthorMetadata>(builder, (edition, book, metadata) =>
|
||||||
{
|
{
|
||||||
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Books
|
|||||||
|
|
||||||
public List<Edition> SetMonitored(Edition edition)
|
public List<Edition> SetMonitored(Edition edition)
|
||||||
{
|
{
|
||||||
var allEditions = FindByBook(edition.BookId);
|
var allEditions = FindByBook(new[] { edition.BookId });
|
||||||
allEditions.ForEach(r => r.Monitored = r.Id == edition.Id);
|
allEditions.ForEach(r => r.Monitored = r.Id == edition.Id);
|
||||||
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
|
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
|
||||||
UpdateMany(allEditions);
|
UpdateMany(allEditions);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public interface ISeriesRepository : IBasicRepository<Series>
|
public interface ISeriesRepository : IBasicRepository<Series>
|
||||||
{
|
{
|
||||||
Series FindById(string foreignSeriesId);
|
Series FindById(string foreignSeriesId);
|
||||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
List<Series> FindById(List<string> foreignSeriesId);
|
||||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||||
List<Series> GetByAuthorId(int authorId);
|
List<Series> GetByAuthorId(int authorId);
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
|
|||||||
return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault();
|
return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Series> FindById(IEnumerable<string> foreignSeriesId)
|
public List<Series> FindById(List<string> foreignSeriesId)
|
||||||
{
|
{
|
||||||
return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId));
|
return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Books
|
|||||||
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||||
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||||
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
||||||
List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
|
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
|
||||||
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
||||||
Book AddBook(Book newBook, bool doRefresh = true);
|
Book AddBook(Book newBook, bool doRefresh = true);
|
||||||
Book FindById(string foreignId);
|
Book FindById(string foreignId);
|
||||||
@@ -206,7 +206,7 @@ namespace NzbDrone.Core.Books
|
|||||||
return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList();
|
return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
|
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
|
||||||
{
|
{
|
||||||
return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds);
|
return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ namespace NzbDrone.Core.Books
|
|||||||
void InsertMany(List<Edition> editions);
|
void InsertMany(List<Edition> editions);
|
||||||
void UpdateMany(List<Edition> editions);
|
void UpdateMany(List<Edition> editions);
|
||||||
void DeleteMany(List<Edition> editions);
|
void DeleteMany(List<Edition> editions);
|
||||||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds);
|
||||||
List<Edition> GetEditionsByBook(int bookId);
|
List<Edition> GetEditionsByBook(int bookId);
|
||||||
|
List<Edition> GetEditionsByBook(IEnumerable<int> bookIds);
|
||||||
List<Edition> GetEditionsByAuthor(int authorId);
|
List<Edition> GetEditionsByAuthor(int authorId);
|
||||||
Edition FindByTitle(int authorMetadataId, string title);
|
Edition FindByTitle(int authorMetadataId, string title);
|
||||||
Edition FindByTitleInexact(int authorMetadataId, string title);
|
Edition FindByTitleInexact(int authorMetadataId, string title);
|
||||||
@@ -72,14 +73,19 @@ namespace NzbDrone.Core.Books
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
|
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds)
|
||||||
{
|
{
|
||||||
return _editionRepository.GetEditionsForRefresh(bookId, foreignEditionIds);
|
return _editionRepository.GetEditionsForRefresh(bookId, foreignEditionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Edition> GetEditionsByBook(int bookId)
|
public List<Edition> GetEditionsByBook(int bookId)
|
||||||
{
|
{
|
||||||
return _editionRepository.FindByBook(bookId);
|
return _editionRepository.FindByBook(new[] { bookId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Edition> GetEditionsByBook(IEnumerable<int> bookIds)
|
||||||
|
{
|
||||||
|
return _editionRepository.FindByBook(bookIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Edition> GetEditionsByAuthor(int authorId)
|
public List<Edition> GetEditionsByAuthor(int authorId)
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Books
|
|||||||
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
|
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
|
||||||
{
|
{
|
||||||
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
|
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
|
||||||
remoteChildren.Select(x => x.ForeignBookId));
|
remoteChildren.Select(x => x.ForeignBookId).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)
|
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles;
|
|||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.MetadataSource;
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
using NzbDrone.Core.RootFolders;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Books
|
namespace NzbDrone.Core.Books
|
||||||
{
|
{
|
||||||
@@ -30,6 +31,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IAuthorService _authorService;
|
private readonly IAuthorService _authorService;
|
||||||
|
private readonly IRootFolderService _rootFolderService;
|
||||||
private readonly IAddAuthorService _addAuthorService;
|
private readonly IAddAuthorService _addAuthorService;
|
||||||
private readonly IEditionService _editionService;
|
private readonly IEditionService _editionService;
|
||||||
private readonly IProvideAuthorInfo _authorInfo;
|
private readonly IProvideAuthorInfo _authorInfo;
|
||||||
@@ -44,6 +46,7 @@ namespace NzbDrone.Core.Books
|
|||||||
|
|
||||||
public RefreshBookService(IBookService bookService,
|
public RefreshBookService(IBookService bookService,
|
||||||
IAuthorService authorService,
|
IAuthorService authorService,
|
||||||
|
IRootFolderService rootFolderService,
|
||||||
IAddAuthorService addAuthorService,
|
IAddAuthorService addAuthorService,
|
||||||
IEditionService editionService,
|
IEditionService editionService,
|
||||||
IAuthorMetadataService authorMetadataService,
|
IAuthorMetadataService authorMetadataService,
|
||||||
@@ -60,6 +63,7 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
_bookService = bookService;
|
_bookService = bookService;
|
||||||
_authorService = authorService;
|
_authorService = authorService;
|
||||||
|
_rootFolderService = rootFolderService;
|
||||||
_addAuthorService = addAuthorService;
|
_addAuthorService = addAuthorService;
|
||||||
_editionService = editionService;
|
_editionService = editionService;
|
||||||
_authorInfo = authorInfo;
|
_authorInfo = authorInfo;
|
||||||
@@ -142,7 +146,7 @@ namespace NzbDrone.Core.Books
|
|||||||
Metadata = remote.AuthorMetadata.Value,
|
Metadata = remote.AuthorMetadata.Value,
|
||||||
MetadataProfileId = oldAuthor.MetadataProfileId,
|
MetadataProfileId = oldAuthor.MetadataProfileId,
|
||||||
QualityProfileId = oldAuthor.QualityProfileId,
|
QualityProfileId = oldAuthor.QualityProfileId,
|
||||||
RootFolderPath = oldAuthor.RootFolderPath,
|
RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldAuthor.Path),
|
||||||
Monitored = oldAuthor.Monitored,
|
Monitored = oldAuthor.Monitored,
|
||||||
Tags = oldAuthor.Tags
|
Tags = oldAuthor.Tags
|
||||||
};
|
};
|
||||||
@@ -246,7 +250,7 @@ namespace NzbDrone.Core.Books
|
|||||||
|
|
||||||
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
|
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
|
||||||
{
|
{
|
||||||
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId));
|
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
|
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace NzbDrone.Core.Books
|
|||||||
var updated = false;
|
var updated = false;
|
||||||
|
|
||||||
var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId);
|
var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId);
|
||||||
var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId));
|
var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId).ToList());
|
||||||
var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList();
|
var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList();
|
||||||
|
|
||||||
var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);
|
var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace NzbDrone.Core.Books
|
|||||||
public interface ISeriesService
|
public interface ISeriesService
|
||||||
{
|
{
|
||||||
Series FindById(string foreignSeriesId);
|
Series FindById(string foreignSeriesId);
|
||||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
List<Series> FindById(List<string> foreignSeriesId);
|
||||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||||
List<Series> GetByAuthorId(int authorId);
|
List<Series> GetByAuthorId(int authorId);
|
||||||
void Delete(int seriesId);
|
void Delete(int seriesId);
|
||||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Books
|
|||||||
return _seriesRepository.FindById(foreignSeriesId);
|
return _seriesRepository.FindById(foreignSeriesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Series> FindById(IEnumerable<string> foreignSeriesId)
|
public List<Series> FindById(List<string> foreignSeriesId)
|
||||||
{
|
{
|
||||||
return _seriesRepository.FindById(foreignSeriesId);
|
return _seriesRepository.FindById(foreignSeriesId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Authentication;
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
@@ -18,7 +20,7 @@ using NzbDrone.Core.Update;
|
|||||||
namespace NzbDrone.Core.Configuration
|
namespace NzbDrone.Core.Configuration
|
||||||
{
|
{
|
||||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||||
IExecute<ResetApiKeyCommand>
|
IExecute<ResetApiKeyCommand>
|
||||||
{
|
{
|
||||||
Dictionary<string, object> GetConfigDictionary();
|
Dictionary<string, object> GetConfigDictionary();
|
||||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||||
@@ -48,6 +50,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string SyslogServer { get; }
|
string SyslogServer { get; }
|
||||||
int SyslogPort { get; }
|
int SyslogPort { get; }
|
||||||
string SyslogLevel { get; }
|
string SyslogLevel { get; }
|
||||||
|
string PostgresHost { get; }
|
||||||
|
int PostgresPort { get; }
|
||||||
|
string PostgresUser { get; }
|
||||||
|
string PostgresPassword { get; }
|
||||||
|
string PostgresMainDb { get; }
|
||||||
|
string PostgresLogDb { get; }
|
||||||
|
string PostgresCacheDb { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConfigFileProvider : IConfigFileProvider
|
public class ConfigFileProvider : IConfigFileProvider
|
||||||
@@ -57,6 +66,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly ICached<string> _cache;
|
private readonly ICached<string> _cache;
|
||||||
|
private readonly PostgresOptions _postgresOptions;
|
||||||
|
|
||||||
private readonly string _configFile;
|
private readonly string _configFile;
|
||||||
|
|
||||||
@@ -65,12 +75,14 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
|
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
IDiskProvider diskProvider)
|
IDiskProvider diskProvider,
|
||||||
|
IOptions<PostgresOptions> postgresOptions)
|
||||||
{
|
{
|
||||||
_cache = cacheManager.GetCache<string>(GetType());
|
_cache = cacheManager.GetCache<string>(GetType());
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_configFile = appFolderInfo.GetConfigPath();
|
_configFile = appFolderInfo.GetConfigPath();
|
||||||
|
_postgresOptions = postgresOptions.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, object> GetConfigDictionary()
|
public Dictionary<string, object> GetConfigDictionary()
|
||||||
@@ -184,6 +196,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
|
|
||||||
public string LogLevel => GetValue("LogLevel", "info");
|
public string LogLevel => GetValue("LogLevel", "info");
|
||||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||||
|
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
|
||||||
|
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
|
||||||
|
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
|
||||||
|
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "readarr-main", persist: false);
|
||||||
|
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "readarr-log", persist: false);
|
||||||
|
public string PostgresCacheDb => _postgresOptions?.CacheDb ?? GetValue("PostgresCacheDb", "readarr-cache", persist: false);
|
||||||
|
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
||||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||||
|
|||||||
@@ -410,6 +410,8 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public CertificateValidationType CertificateValidation =>
|
public CertificateValidationType CertificateValidation =>
|
||||||
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
|
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
|
||||||
|
|
||||||
|
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||||
|
|
||||||
private string GetValue(string key)
|
private string GetValue(string key)
|
||||||
{
|
{
|
||||||
return GetValue(key, string.Empty);
|
return GetValue(key, string.Empty);
|
||||||
|
|||||||
@@ -97,5 +97,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
int BackupRetention { get; }
|
int BackupRetention { get; }
|
||||||
|
|
||||||
CertificateValidationType CertificateValidation { get; }
|
CertificateValidationType CertificateValidation { get; }
|
||||||
|
string ApplicationUrl { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
_updateSql = GetUpdateSql(_properties);
|
_updateSql = GetUpdateSql(_properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual SqlBuilder Builder() => new SqlBuilder();
|
protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType);
|
||||||
|
|
||||||
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();
|
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
using (var conn = _database.OpenConnection())
|
using (var conn = _database.OpenConnection())
|
||||||
{
|
{
|
||||||
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}");
|
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM \"{_table}\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +176,11 @@ namespace NzbDrone.Core.Datastore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||||
|
{
|
||||||
|
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
|
||||||
|
}
|
||||||
|
|
||||||
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +199,8 @@ namespace NzbDrone.Core.Datastore
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = (int)multi.Read().First().id;
|
var multiRead = multi.Read();
|
||||||
|
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||||
_keyProperty.SetValue(model, id);
|
_keyProperty.SetValue(model, id);
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
@@ -305,7 +311,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
using (var conn = _database.OpenConnection())
|
using (var conn = _database.OpenConnection())
|
||||||
{
|
{
|
||||||
conn.Execute($"DELETE FROM [{_table}]");
|
conn.Execute($"DELETE FROM \"{_table}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vacuum)
|
if (vacuum)
|
||||||
@@ -364,7 +370,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
|
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendFormat("UPDATE {0} SET ", _table);
|
sb.AppendFormat("UPDATE \"{0}\" SET ", _table);
|
||||||
|
|
||||||
for (var i = 0; i < propertiesToUpdate.Count; i++)
|
for (var i = 0; i < propertiesToUpdate.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -446,9 +452,10 @@ namespace NzbDrone.Core.Datastore
|
|||||||
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
|
||||||
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
||||||
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
|
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
|
||||||
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||||
|
|
||||||
return queryFunc(builder).ToList();
|
return queryFunc(builder).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
|
|||||||
public class CacheDatabase : ICacheDatabase
|
public class CacheDatabase : ICacheDatabase
|
||||||
{
|
{
|
||||||
private readonly IDatabase _database;
|
private readonly IDatabase _database;
|
||||||
|
private readonly DatabaseType _databaseType;
|
||||||
|
|
||||||
public CacheDatabase(IDatabase database)
|
public CacheDatabase(IDatabase database)
|
||||||
{
|
{
|
||||||
_database = database;
|
_database = database;
|
||||||
|
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDbConnection OpenConnection()
|
public IDbConnection OpenConnection()
|
||||||
@@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
public int Migration => _database.Migration;
|
public int Migration => _database.Migration;
|
||||||
|
|
||||||
|
public DatabaseType DatabaseType => _databaseType;
|
||||||
|
|
||||||
public void Vacuum()
|
public void Vacuum()
|
||||||
{
|
{
|
||||||
_database.Vacuum();
|
_database.Vacuum();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
|
using Npgsql;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
@@ -15,11 +17,20 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
public class ConnectionStringFactory : IConnectionStringFactory
|
public class ConnectionStringFactory : IConnectionStringFactory
|
||||||
{
|
{
|
||||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
|
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
|
||||||
{
|
{
|
||||||
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
|
_configFileProvider = configFileProvider;
|
||||||
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
|
|
||||||
CacheDbConnectionString = GetConnectionString(appFolderInfo.GetCacheDatabase());
|
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||||
|
GetConnectionString(appFolderInfo.GetDatabase());
|
||||||
|
|
||||||
|
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||||
|
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||||
|
|
||||||
|
CacheDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
|
||||||
|
GetConnectionString(appFolderInfo.GetCacheDatabase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MainDbConnectionString { get; private set; }
|
public string MainDbConnectionString { get; private set; }
|
||||||
@@ -51,5 +62,19 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
return connectionBuilder.ConnectionString;
|
return connectionBuilder.ConnectionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetPostgresConnectionString(string dbName)
|
||||||
|
{
|
||||||
|
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
||||||
|
|
||||||
|
connectionBuilder.Database = dbName;
|
||||||
|
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
||||||
|
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
||||||
|
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
||||||
|
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
||||||
|
connectionBuilder.Enlist = false;
|
||||||
|
|
||||||
|
return connectionBuilder.ConnectionString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user