mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-10 15:10:02 -04:00
Compare commits
75 Commits
sonarr-pul
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925569a17b | ||
|
|
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 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -42,12 +42,14 @@ body:
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
- **Database**: Sqlite 3.36.0
|
||||
value: |
|
||||
- OS:
|
||||
- Readarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Database:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<development@readarr.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities on Discord (preferred) to
|
||||
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
|
||||
us within 72 hours. If the issue is confirmed, we will release a patch as soon
|
||||
as possible depending on complexity/severity.
|
||||
@@ -15,7 +15,7 @@ variables:
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.300'
|
||||
dotnetVersion: '6.0.302'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
@@ -529,6 +529,57 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: LinuxCoreTests
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
@@ -597,6 +648,66 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
workspace:
|
||||
@@ -619,7 +730,7 @@ stages:
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '/$(pattern)'
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- bash: |
|
||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||
@@ -939,3 +1050,4 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'filenames',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
@@ -308,7 +309,9 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 2,
|
||||
'react/jsx-wrap-multilines': 2
|
||||
'react/jsx-wrap-multilines': 2,
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -169,6 +169,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import Portal from 'Components/Portal';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
|
||||
@@ -33,7 +33,7 @@ function ConfirmModal(props) {
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [isOpen, onConfirm]);
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { isIOS } from 'Utilities/browser';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isIOS } from 'Utilities/mobile';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import ModalError from './ModalError';
|
||||
import styles from './Modal.css';
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._isMobile = isMobileUtil();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -41,10 +30,8 @@ class PageContentBody extends Component {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
@@ -53,7 +40,7 @@ class PageContentBody extends Component {
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
</div>
|
||||
</ScrollerComponent>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.jumpBar {
|
||||
z-index: $pageJumpBarZIndex;
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './OverlayScroller.css';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'Components/Measure';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import styles from './SwipeHeader.css';
|
||||
|
||||
function cursorPosition(event) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Portal from 'Components/Portal';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
let maxWidth = null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
@@ -9,7 +10,8 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import SelectEditionRow from './SelectEditionRow';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectEditionRowConnector from './SelectEditionRowConnector';
|
||||
import styles from './SelectEditionModalContent.css';
|
||||
|
||||
const columns = [
|
||||
@@ -33,15 +35,30 @@ class SelectEditionModalContent extends Component {
|
||||
render() {
|
||||
const {
|
||||
books,
|
||||
isPopulated,
|
||||
isFetching,
|
||||
error,
|
||||
onEditionSelect,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!isPopulated && !error) {
|
||||
return (<LoadingIndicator />);
|
||||
}
|
||||
|
||||
if (!isFetching && error) {
|
||||
return (
|
||||
<div>
|
||||
{translate('LoadingEditionsFailed')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Edition
|
||||
{translate('ManualImportSelectEdition')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -60,7 +77,7 @@ class SelectEditionModalContent extends Component {
|
||||
{
|
||||
books.map((item) => {
|
||||
return (
|
||||
<SelectEditionRow
|
||||
<SelectEditionRowConnector
|
||||
key={item.book.id}
|
||||
matchedEditionId={item.matchedEditionId}
|
||||
columns={columns}
|
||||
@@ -76,7 +93,7 @@ class SelectEditionModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
@@ -86,6 +103,9 @@ class SelectEditionModalContent extends Component {
|
||||
|
||||
SelectEditionModalContent.propTypes = {
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isFetching: PropTypes.bool,
|
||||
isPopulated: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
onEditionSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,27 +1,71 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
|
||||
import {
|
||||
saveInteractiveImportItem,
|
||||
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import SelectEditionModalContent from './SelectEditionModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return {};
|
||||
return createSelector(
|
||||
(state) => state.editions,
|
||||
(editions) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = editions;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchEditions,
|
||||
clearEditions,
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectEditionModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.populate);
|
||||
this.populate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.populate);
|
||||
this.unpopulate();
|
||||
}
|
||||
//
|
||||
// Control
|
||||
|
||||
populate = () => {
|
||||
const bookId = this.props.books.map((b) => b.book.id);
|
||||
|
||||
this.props.fetchEditions({ bookId });
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.clearEditions();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditionSelect = (bookId, foreignEditionId) => {
|
||||
console.log(`book: ${bookId} id: ${foreignEditionId} ${typeof foreignEditionId}`);
|
||||
const ids = this.props.importIdsByBook[bookId];
|
||||
|
||||
ids.forEach((id) => {
|
||||
@@ -55,6 +99,8 @@ class SelectEditionModalContentConnector extends Component {
|
||||
SelectEditionModalContentConnector.propTypes = {
|
||||
importIdsByBook: PropTypes.object.isRequired,
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchEditions: PropTypes.func.isRequired,
|
||||
clearEditions: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import SelectEditionRow from './SelectEditionRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.editions,
|
||||
(id, editionState) => {
|
||||
const editions = editionState.items.filter((e) => e.bookId === id);
|
||||
return { editions };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class SelectEditionRowConnector extends Component {
|
||||
render() {
|
||||
return (
|
||||
<SelectEditionRow
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionRowConnector.PropTypes = {
|
||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(SelectEditionRowConnector);
|
||||
@@ -148,11 +148,10 @@ class AddNewItem extends Component {
|
||||
);
|
||||
} else if (item.book) {
|
||||
const book = item.book;
|
||||
const edition = book.editions.find((x) => x.monitored);
|
||||
return (
|
||||
<AddNewBookSearchResultConnector
|
||||
key={item.id}
|
||||
isExistingBook={'id' in edition && edition.id !== 0}
|
||||
isExistingBook={'id' in book && book.id !== 0}
|
||||
isExistingAuthor={'id' in book.author && book.author.id !== 0}
|
||||
{...book}
|
||||
/>
|
||||
|
||||
@@ -138,17 +138,20 @@ class AddNewBookSearchResult extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
||||
onPress={this.onTVDBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
{
|
||||
editions && editions.length > 1 ?
|
||||
<Link
|
||||
className={styles.mbLink}
|
||||
to={`https://goodreads.com/book/show/${editions[0].foreignEditionId}`}
|
||||
onPress={this.onTVDBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.mbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link> : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,7 +221,7 @@ AddNewBookSearchResult.propTypes = {
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
author: PropTypes.object,
|
||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
editions: PropTypes.arrayOf(PropTypes.object),
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingBook: PropTypes.bool.isRequired,
|
||||
isExistingAuthor: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -173,7 +173,7 @@ class AddAuthorOptionsForm extends Component {
|
||||
AddAuthorOptionsForm.propTypes = {
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
monitorNewItems: PropTypes.string.isRequired,
|
||||
monitorNewItems: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
metadataProfileId: PropTypes.object,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -21,6 +21,7 @@ function HostSettings(props) {
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
applicationUrl,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertPath,
|
||||
@@ -58,6 +59,7 @@ function HostSettings(props) {
|
||||
name="port"
|
||||
min={1}
|
||||
max={65535}
|
||||
autocomplete="off"
|
||||
helpTextWarning={translate('PortHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...port}
|
||||
@@ -95,6 +97,21 @@ function HostSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ApplicationURL')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="applicationUrl"
|
||||
helpText={translate('ApplicationUrlHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...applicationUrl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -19,7 +19,7 @@ function PendingChangesModal(props) {
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [onConfirm]);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -6,6 +6,7 @@ import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { removeItem, set, updateItem } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastSaveData = null;
|
||||
|
||||
export function createCancelSaveProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -27,27 +28,33 @@ function createSaveProviderHandler(section, url, options = {}, removeStale = fal
|
||||
} = payload;
|
||||
|
||||
const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section);
|
||||
const requestUrl = id ? `${url}/${id}` : url;
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-saving the same provider without changes
|
||||
// force it to be saved. Only applies to editing existing providers.
|
||||
|
||||
if (id && _.isEqual(saveData, lastSaveData)) {
|
||||
params.forceSave = true;
|
||||
}
|
||||
|
||||
lastSaveData = saveData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
method: 'POST',
|
||||
url: `${requestUrl}?${$.param(params, true)}`,
|
||||
method: id ? 'PUT' : 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(saveData)
|
||||
};
|
||||
|
||||
if (id) {
|
||||
ajaxOptions.method = 'PUT';
|
||||
if (!Array.isArray(id)) {
|
||||
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastSaveData = null;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ export const defaultState = {
|
||||
label: 'Release Group',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
columnLabel: 'Details',
|
||||
|
||||
@@ -177,7 +177,8 @@ export const defaultState = {
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'seeders',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import createClientSideCollectionSelector from './createClientSideCollectionSelector';
|
||||
import createBooksClientSideCollectionSelector from './createBooksClientSideCollectionSelector';
|
||||
|
||||
function createUnoptimizedSelector(uiSection) {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('books', uiSection),
|
||||
createBooksClientSideCollectionSelector(uiSection),
|
||||
(books) => {
|
||||
const items = books.items.map((s) => {
|
||||
const {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import filterCollection from 'Utilities/Array/filterCollection';
|
||||
import sortCollection from 'Utilities/Array/sortCollection';
|
||||
import createCustomFiltersSelector from './createCustomFiltersSelector';
|
||||
|
||||
function createBooksClientSideCollectionSelector(uiSection) {
|
||||
return createSelector(
|
||||
(state) => _.get(state, 'books'),
|
||||
(state) => _.get(state, 'authors'),
|
||||
(state) => _.get(state, uiSection),
|
||||
createCustomFiltersSelector('books', uiSection),
|
||||
(bookState, authorState, uiSectionState = {}, customFilters) => {
|
||||
const state = Object.assign({}, bookState, uiSectionState, { customFilters });
|
||||
|
||||
const books = state.items;
|
||||
for (const book of books) {
|
||||
book.author = authorState.items[authorState.itemMap[book.authorId]];
|
||||
}
|
||||
|
||||
const filtered = filterCollection(books, state);
|
||||
const sorted = sortCollection(filtered, state);
|
||||
|
||||
return {
|
||||
...bookState,
|
||||
...uiSectionState,
|
||||
customFilters,
|
||||
items: sorted,
|
||||
totalItems: state.items.length
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createBooksClientSideCollectionSelector;
|
||||
@@ -1,123 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||
|
||||
function getSortClause(sortKey, sortDirection, sortPredicates) {
|
||||
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
|
||||
return function(item) {
|
||||
return sortPredicates[sortKey](item, sortDirection);
|
||||
};
|
||||
}
|
||||
|
||||
return function(item) {
|
||||
return item[sortKey];
|
||||
};
|
||||
}
|
||||
|
||||
function filter(items, state) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
filterPredicates
|
||||
} = state;
|
||||
|
||||
if (!selectedFilterKey) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||
|
||||
return _.filter(items, (item) => {
|
||||
let i = 0;
|
||||
let accepted = true;
|
||||
|
||||
while (accepted && i < selectedFilters.length) {
|
||||
const {
|
||||
key,
|
||||
value,
|
||||
type = filterTypes.EQUAL
|
||||
} = selectedFilters[i];
|
||||
|
||||
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
|
||||
const predicate = filterPredicates[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
type === filterTypes.NOT_CONTAINS ||
|
||||
type === filterTypes.NOT_EQUAL
|
||||
) {
|
||||
accepted = value.every((v) => predicate(item, v, type));
|
||||
} else {
|
||||
accepted = value.some((v) => predicate(item, v, type));
|
||||
}
|
||||
} else {
|
||||
accepted = predicate(item, value, type);
|
||||
}
|
||||
} else if (item.hasOwnProperty(key)) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
type === filterTypes.NOT_CONTAINS ||
|
||||
type === filterTypes.NOT_EQUAL
|
||||
) {
|
||||
accepted = value.every((v) => predicate(item[key], v));
|
||||
} else {
|
||||
accepted = value.some((v) => predicate(item[key], v));
|
||||
}
|
||||
} else {
|
||||
accepted = predicate(item[key], value);
|
||||
}
|
||||
} else {
|
||||
// Default to false if the filter can't be tested
|
||||
accepted = false;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return accepted;
|
||||
});
|
||||
}
|
||||
|
||||
function sort(items, state) {
|
||||
const {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
sortPredicates,
|
||||
secondarySortKey,
|
||||
secondarySortDirection
|
||||
} = state;
|
||||
|
||||
const clauses = [];
|
||||
const orders = [];
|
||||
|
||||
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
|
||||
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
||||
|
||||
if (secondarySortKey &&
|
||||
secondarySortDirection &&
|
||||
(sortKey !== secondarySortKey ||
|
||||
sortDirection !== secondarySortDirection)) {
|
||||
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
|
||||
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
||||
}
|
||||
|
||||
return _.orderBy(items, clauses, orders);
|
||||
}
|
||||
|
||||
function createCustomFiltersSelector(type, alternateType) {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.items,
|
||||
(customFilters) => {
|
||||
return customFilters.filter((customFilter) => {
|
||||
return customFilter.type === type || customFilter.type === alternateType;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
import filterCollection from 'Utilities/Array/filterCollection';
|
||||
import sortCollection from 'Utilities/Array/sortCollection';
|
||||
import createCustomFiltersSelector from './createCustomFiltersSelector';
|
||||
|
||||
function createClientSideCollectionSelector(section, uiSection) {
|
||||
return createSelector(
|
||||
@@ -127,8 +12,8 @@ function createClientSideCollectionSelector(section, uiSection) {
|
||||
(sectionState, uiSectionState = {}, customFilters) => {
|
||||
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
||||
|
||||
const filtered = filter(state.items, state);
|
||||
const sorted = sort(filtered, state);
|
||||
const filtered = filterCollection(state.items, state);
|
||||
const sorted = sortCollection(filtered, state);
|
||||
|
||||
return {
|
||||
...sectionState,
|
||||
|
||||
14
frontend/src/Store/Selectors/createCustomFiltersSelector.js
Normal file
14
frontend/src/Store/Selectors/createCustomFiltersSelector.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createCustomFiltersSelector(type, alternateType) {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.items,
|
||||
(customFilters) => {
|
||||
return customFilters.filter((customFilter) => {
|
||||
return customFilter.type === type || customFilter.type === alternateType;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCustomFiltersSelector;
|
||||
@@ -1,4 +1,7 @@
|
||||
@define-mixin scrollbar {
|
||||
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
pageJumpBarZIndex: 10,
|
||||
modalZIndex: 1000,
|
||||
popperZIndex: 2000
|
||||
};
|
||||
|
||||
@@ -20,10 +20,11 @@ class About extends Component {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isMono,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -48,14 +49,6 @@ class About extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<DescriptionListItem
|
||||
title={translate('MonoVersion')}
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
@@ -77,6 +70,11 @@ class About extends Component {
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -114,10 +112,11 @@ About.propTypes = {
|
||||
packageVersion: PropTypes.string,
|
||||
packageAuthor: PropTypes.string,
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
|
||||
72
frontend/src/Utilities/Array/filterCollection.js
Normal file
72
frontend/src/Utilities/Array/filterCollection.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import _ from 'lodash';
|
||||
import { filterTypePredicates, filterTypes } from 'Helpers/Props';
|
||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||
|
||||
function filterCollection(items, state) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
filterPredicates
|
||||
} = state;
|
||||
|
||||
if (!selectedFilterKey) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||
|
||||
return _.filter(items, (item) => {
|
||||
let i = 0;
|
||||
let accepted = true;
|
||||
|
||||
while (accepted && i < selectedFilters.length) {
|
||||
const {
|
||||
key,
|
||||
value,
|
||||
type = filterTypes.EQUAL
|
||||
} = selectedFilters[i];
|
||||
|
||||
if (filterPredicates && filterPredicates.hasOwnProperty(key)) {
|
||||
const predicate = filterPredicates[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
type === filterTypes.NOT_CONTAINS ||
|
||||
type === filterTypes.NOT_EQUAL
|
||||
) {
|
||||
accepted = value.every((v) => predicate(item, v, type));
|
||||
} else {
|
||||
accepted = value.some((v) => predicate(item, v, type));
|
||||
}
|
||||
} else {
|
||||
accepted = predicate(item, value, type);
|
||||
}
|
||||
} else if (item.hasOwnProperty(key)) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
type === filterTypes.NOT_CONTAINS ||
|
||||
type === filterTypes.NOT_EQUAL
|
||||
) {
|
||||
accepted = value.every((v) => predicate(item[key], v));
|
||||
} else {
|
||||
accepted = value.some((v) => predicate(item[key], v));
|
||||
}
|
||||
} else {
|
||||
accepted = predicate(item[key], value);
|
||||
}
|
||||
} else {
|
||||
// Default to false if the filter can't be tested
|
||||
accepted = false;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return accepted;
|
||||
});
|
||||
}
|
||||
|
||||
export default filterCollection;
|
||||
42
frontend/src/Utilities/Array/sortCollection.js
Normal file
42
frontend/src/Utilities/Array/sortCollection.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import _ from 'lodash';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
|
||||
function getSortClause(sortKey, sortDirection, sortPredicates) {
|
||||
if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) {
|
||||
return function(item) {
|
||||
return sortPredicates[sortKey](item, sortDirection);
|
||||
};
|
||||
}
|
||||
|
||||
return function(item) {
|
||||
return item[sortKey];
|
||||
};
|
||||
}
|
||||
|
||||
function sortCollection(items, state) {
|
||||
const {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
sortPredicates,
|
||||
secondarySortKey,
|
||||
secondarySortDirection
|
||||
} = state;
|
||||
|
||||
const clauses = [];
|
||||
const orders = [];
|
||||
|
||||
clauses.push(getSortClause(sortKey, sortDirection, sortPredicates));
|
||||
orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
||||
|
||||
if (secondarySortKey &&
|
||||
secondarySortDirection &&
|
||||
(sortKey !== secondarySortKey ||
|
||||
sortDirection !== secondarySortDirection)) {
|
||||
clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates));
|
||||
orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc');
|
||||
}
|
||||
|
||||
return _.orderBy(items, clauses, orders);
|
||||
}
|
||||
|
||||
export default sortCollection;
|
||||
@@ -10,3 +10,7 @@ export function isMobile() {
|
||||
export function isIOS() {
|
||||
return mobileDetect.is('iOS');
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "6.0.5",
|
||||
"@microsoft/signalr": "6.0.7",
|
||||
"@sentry/browser": "6.18.2",
|
||||
"@sentry/integrations": "6.18.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
@@ -58,7 +58,7 @@
|
||||
"react-addons-shallow-compare": "15.6.3",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-custom-scrollbars-2": "4.5.0",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dnd-multi-backend": "6.0.2",
|
||||
@@ -109,6 +109,7 @@
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-react": "7.24.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "3.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
|
||||
<PackageVersion Include="Dapper" Version="2.0.123" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="4.8.7" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="5.2.0" />
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.2" />
|
||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.289" />
|
||||
<PackageVersion Include="FluentMigrator.Runner" Version="4.0.0-alpha.289" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageVersion Include="FluentValidation" Version="8.6.2" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||
<PackageVersion Include="LazyCache" Version="2.4.0" />
|
||||
<PackageVersion Include="Mailkit" Version="3.1.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.5" />
|
||||
<PackageVersion Include="Mailkit" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
@@ -24,41 +25,41 @@
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr22" />
|
||||
<PackageVersion Include="MonoTorrent" Version="2.0.5" />
|
||||
<PackageVersion Include="Moq" Version="4.17.2" />
|
||||
<PackageVersion Include="MonoTorrent" Version="2.0.6" />
|
||||
<PackageVersion Include="NBuilder" Version="6.1.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageVersion Include="NLog" Version="4.7.14" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.1.0" />
|
||||
<PackageVersion Include="NLog" Version="5.0.5" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageVersion Include="Npgsql" Version="6.0.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.2" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.18" />
|
||||
<PackageVersion Include="PdfSharpCore" Version="1.3.32" />
|
||||
<PackageVersion Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||
<PackageVersion Include="RestSharp" Version="106.15.0" />
|
||||
<PackageVersion Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
|
||||
<PackageVersion Include="Sentry" Version="3.14.1" />
|
||||
<PackageVersion Include="Sentry" Version="3.20.1" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.5.1" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="16.1.20" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="16.1.20" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.4" />
|
||||
<PackageVersion Include="System.Memory" Version="4.5.5" />
|
||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.4" />
|
||||
<PackageVersion Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
|
||||
<PackageVersion Include="Unity" Version="5.11.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
|
||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||
@@ -17,10 +17,6 @@
|
||||
<packageSource key="dotnet-bsd-crossbuild">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="FluentMigrator">
|
||||
<package pattern="FluentMigrator" />
|
||||
<package pattern="FluentMigrator.*" />
|
||||
</packageSource>
|
||||
<packageSource key="Mono.Posix.NETStandard">
|
||||
<package pattern="Mono.Posix.NETStandard" />
|
||||
</packageSource>
|
||||
@@ -30,5 +26,8 @@
|
||||
<packageSource key="coverlet-nightly">
|
||||
<package pattern="coverlet.*" />
|
||||
</packageSource>
|
||||
<packageSource key="FluentMigrator">
|
||||
<package pattern="Servarr.FluentMigrator*"/>
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
|
||||
|
||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
|
||||
where TSubject : class, IDiskProvider
|
||||
{
|
||||
[SetUp]
|
||||
public void BaseSetup()
|
||||
{
|
||||
Mocker.SetConstant<IFileSystem>(new FileSystem());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void writealltext_should_truncate_existing()
|
||||
{
|
||||
|
||||
@@ -437,24 +437,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_not_copy_casesensitive_folder()
|
||||
{
|
||||
MonoOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
// Note: Although technically possible top copy to different case, we're not allowing it
|
||||
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CopyFolder_should_ignore_nfs_temp_file()
|
||||
{
|
||||
@@ -540,26 +522,6 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
source.FullName.GetActualCasing().Should().Be(destination.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MoveFolder_should_rename_casesensitive_folder()
|
||||
{
|
||||
MonoOnly();
|
||||
|
||||
WithRealDiskProvider();
|
||||
|
||||
var original = GetFilledTempFolder();
|
||||
var root = new DirectoryInfo(GetTempFilePath());
|
||||
var source = new DirectoryInfo(root.FullName + "A/series");
|
||||
var destination = new DirectoryInfo(root.FullName + "A/Series");
|
||||
|
||||
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
|
||||
|
||||
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
|
||||
|
||||
Directory.Exists(source.FullName).Should().Be(false);
|
||||
Directory.Exists(destination.FullName).Should().Be(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_destination_is_readonly()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -10,6 +11,12 @@ namespace NzbDrone.Common.Test.DiskTests
|
||||
public abstract class FreeSpaceFixtureBase<TSubject> : TestBase<TSubject>
|
||||
where TSubject : class, IDiskProvider
|
||||
{
|
||||
[SetUp]
|
||||
public void BaseSetup()
|
||||
{
|
||||
Mocker.SetConstant<IFileSystem>(new FileSystem());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_free_space_for_folder()
|
||||
{
|
||||
|
||||
@@ -207,6 +207,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||
public void should_execute_get_using_brotli()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||
@@ -307,11 +308,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
[Test]
|
||||
public void should_follow_redirects_to_https()
|
||||
{
|
||||
if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
|
||||
{
|
||||
Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
|
||||
}
|
||||
|
||||
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
|
||||
.AddQueryParam("url", $"https://readarr.com/")
|
||||
.Build();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
@@ -70,14 +72,38 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
|
||||
// Notifiarr
|
||||
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
||||
|
||||
// Discord
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().NotContain("mySecret");
|
||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||
cleansedMessage.Should().NotContain("01233210");
|
||||
}
|
||||
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
public void should_keep_message(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().NotContain("mySecret");
|
||||
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
|
||||
cleansedMessage.Should().NotContain("01233210");
|
||||
|
||||
cleansedMessage.Should().Contain("shouldkeep1");
|
||||
cleansedMessage.Should().Contain("shouldkeep2");
|
||||
cleansedMessage.Should().Contain("shouldkeep3");
|
||||
}
|
||||
|
||||
//GoodReads
|
||||
[TestCase(@"{""signatureMethod"": ""hmacSha1"",""signatureTreatment"": ""escaped"",""type"": ""protectedResource"",""method"": ""GET"",""token"": ""mytoken"",""tokenSecret"": ""mytokensecret"",""requestUrl"": ""https://www.goodreads.com/review/list.xml"",""parameters"": { ""_nc"": ""1"", ""v"": ""2"", ""id"": ""999999999"", ""shelf"": ""currently-reading"", ""per_page"": ""200"", ""page"": ""1""}")]
|
||||
[TestCase(@"https://www.goodreads.com/series/311911?key=1234530f422f4aacb6b301233210aaaa&_nc=1&format=xml")]
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace NzbDrone.Common.Test
|
||||
[Test]
|
||||
public void GetUpdateClientExePath()
|
||||
{
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update.exe".AsOsAgnostic());
|
||||
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\readarr_update\Readarr.Update".AsOsAgnostic().ProcessNameToExe());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -171,7 +171,7 @@ namespace NzbDrone.Common.Test
|
||||
var processStarted = new ManualResetEventSlim();
|
||||
|
||||
string suffix;
|
||||
if (OsInfo.IsWindows || PlatformInfo.IsMono)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
suffix = ".exe";
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
|
||||
.AddDummyDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
||||
"/boot",
|
||||
"/lib",
|
||||
"/sbin",
|
||||
"/proc"
|
||||
"/proc",
|
||||
"/usr/bin"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
public enum PlatformType
|
||||
{
|
||||
DotNet = 0,
|
||||
Mono = 1,
|
||||
NetCore = 2
|
||||
}
|
||||
|
||||
public interface IPlatformInfo
|
||||
{
|
||||
Version Version { get; }
|
||||
@@ -19,38 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
public class PlatformInfo : IPlatformInfo
|
||||
{
|
||||
private static readonly Regex MonoVersionRegex = new Regex(@"(?<=\W|^)(?<version>\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static PlatformType _platform;
|
||||
private static Version _version;
|
||||
|
||||
static PlatformInfo()
|
||||
{
|
||||
_platform = PlatformType.NetCore;
|
||||
_version = Environment.Version;
|
||||
}
|
||||
|
||||
public static PlatformType Platform => _platform;
|
||||
public static bool IsMono => Platform == PlatformType.Mono;
|
||||
public static bool IsDotNet => Platform == PlatformType.DotNet;
|
||||
public static bool IsNetCore => Platform == PlatformType.NetCore;
|
||||
|
||||
public static string PlatformName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDotNet)
|
||||
{
|
||||
return ".NET";
|
||||
}
|
||||
else if (IsMono)
|
||||
{
|
||||
return "Mono";
|
||||
}
|
||||
else
|
||||
{
|
||||
return ".NET Core";
|
||||
}
|
||||
return ".NET";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,107 +30,5 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
{
|
||||
return _version;
|
||||
}
|
||||
|
||||
private static Version GetMonoVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = Type.GetType("Mono.Runtime");
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
var displayNameMethod = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (displayNameMethod != null)
|
||||
{
|
||||
var displayName = displayNameMethod.Invoke(null, null).ToString();
|
||||
var versionMatch = MonoVersionRegex.Match(displayName);
|
||||
|
||||
if (versionMatch.Success)
|
||||
{
|
||||
return new Version(versionMatch.Groups["version"].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Couldnt get Mono version: " + ex.ToString());
|
||||
}
|
||||
|
||||
return new Version();
|
||||
}
|
||||
|
||||
private static Version GetDotNetVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";
|
||||
using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey))
|
||||
{
|
||||
if (ndpKey == null)
|
||||
{
|
||||
return new Version(4, 0);
|
||||
}
|
||||
|
||||
var releaseKey = (int)ndpKey.GetValue("Release");
|
||||
|
||||
if (releaseKey >= 528040)
|
||||
{
|
||||
return new Version(4, 8, 0);
|
||||
}
|
||||
|
||||
if (releaseKey >= 461808)
|
||||
{
|
||||
return new Version(4, 7, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 461308)
|
||||
{
|
||||
return new Version(4, 7, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 460798)
|
||||
{
|
||||
return new Version(4, 7);
|
||||
}
|
||||
|
||||
if (releaseKey >= 394802)
|
||||
{
|
||||
return new Version(4, 6, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 394254)
|
||||
{
|
||||
return new Version(4, 6, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 393295)
|
||||
{
|
||||
return new Version(4, 6);
|
||||
}
|
||||
|
||||
if (releaseKey >= 379893)
|
||||
{
|
||||
return new Version(4, 5, 2);
|
||||
}
|
||||
|
||||
if (releaseKey >= 378675)
|
||||
{
|
||||
return new Version(4, 5, 1);
|
||||
}
|
||||
|
||||
if (releaseKey >= 378389)
|
||||
{
|
||||
return new Version(4, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Couldnt get .NET framework version: " + ex.ToString());
|
||||
}
|
||||
|
||||
return new Version(4, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +240,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName, PlatformType runtime)
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
processName += ".exe";
|
||||
}
|
||||
@@ -250,11 +250,6 @@ namespace NzbDrone.Common.Extensions
|
||||
return processName;
|
||||
}
|
||||
|
||||
public static string ProcessNameToExe(this string processName)
|
||||
{
|
||||
return processName.ProcessNameToExe(PlatformInfo.Platform);
|
||||
}
|
||||
|
||||
public static string GetLongestCommonPath(this List<string> paths)
|
||||
{
|
||||
var firstPath = paths.First();
|
||||
@@ -347,9 +342,9 @@ namespace NzbDrone.Common.Extensions
|
||||
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
|
||||
}
|
||||
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
|
||||
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
|
||||
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
|
||||
}
|
||||
|
||||
public static string GetDatabase(this IAppFolderInfo appFolderInfo)
|
||||
|
||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Common.Http
|
||||
StatusCode == HttpStatusCode.Found ||
|
||||
StatusCode == HttpStatusCode.TemporaryRedirect ||
|
||||
StatusCode == HttpStatusCode.RedirectMethod ||
|
||||
StatusCode == HttpStatusCode.SeeOther;
|
||||
StatusCode == HttpStatusCode.SeeOther ||
|
||||
StatusCode == HttpStatusCode.PermanentRedirect;
|
||||
|
||||
public string[] GetCookieHeaders()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -11,13 +11,14 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: )((?: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(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
@@ -46,7 +47,14 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Good Reads
|
||||
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new Regex(@"(?<=""(token|tokensecret)"":\s)""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Webhooks
|
||||
// Notifiarr
|
||||
new Regex(@"api/v[0-9]/notification/readarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Discord
|
||||
new Regex(@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
@@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
|
||||
{
|
||||
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
|
||||
|
||||
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return logBuilder.Property("Sentry", fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
|
||||
}
|
||||
|
||||
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
|
||||
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
|
||||
{
|
||||
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
|
||||
}
|
||||
|
||||
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
|
||||
{
|
||||
SentryLogger.Log(level)
|
||||
.CopyLogEvent(logBuilder.LogEventInfo)
|
||||
SentryLogger.ForLogEvent(level)
|
||||
.CopyLogEvent(logBuilder.LogEvent)
|
||||
.SentryFingerprint(fingerprint)
|
||||
.Write();
|
||||
.Log();
|
||||
|
||||
return logBuilder.Property("Sentry", null);
|
||||
return logBuilder.Property<string>("Sentry", null);
|
||||
}
|
||||
|
||||
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
|
||||
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
|
||||
{
|
||||
return logBuilder.LoggerName(logEvent.LoggerName)
|
||||
.TimeStamp(logEvent.TimeStamp)
|
||||
return logBuilder.TimeStamp(logEvent.TimeStamp)
|
||||
.Message(logEvent.Message, logEvent.Parameters)
|
||||
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
|
||||
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
|
||||
.Exception(logEvent.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +38,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
if ((exception is TypeInitializationException && exception.InnerException is DllNotFoundException) ||
|
||||
exception is DllNotFoundException)
|
||||
{
|
||||
Logger.Debug(exception, "Minor Fail: " + exception.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("EPIC FAIL: {0}", exception);
|
||||
Logger.Fatal(exception, "EPIC FAIL.");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using NLog;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace NzbDrone.Common.Instrumentation
|
||||
{
|
||||
public class NzbDroneFileTarget : FileTarget
|
||||
{
|
||||
protected override string GetFormattedMessage(LogEventInfo logEvent)
|
||||
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
|
||||
{
|
||||
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
|
||||
target.Append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
var appFolderInfo = new AppFolderInfo(startupContext);
|
||||
|
||||
RegisterGlobalFilters();
|
||||
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
RegisterDebugger();
|
||||
@@ -101,6 +103,16 @@ namespace NzbDrone.Common.Instrumentation
|
||||
LogManager.Configuration.LoggingRules.Add(loggingRule);
|
||||
}
|
||||
|
||||
private static void RegisterGlobalFilters()
|
||||
{
|
||||
LogManager.Setup().LoadConfiguration(c =>
|
||||
{
|
||||
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
|
||||
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
|
||||
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
|
||||
});
|
||||
}
|
||||
|
||||
private static void RegisterConsole()
|
||||
{
|
||||
var level = LogLevel.Trace;
|
||||
|
||||
@@ -109,13 +109,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Debug = false;
|
||||
o.DiagnosticLevel = SentryLevel.Debug;
|
||||
o.Release = BuildInfo.Release;
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// Mono 6.0 broke GzipStream.WriteAsync
|
||||
// TODO: Check specific version
|
||||
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
|
||||
}
|
||||
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
@@ -158,14 +151,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
|
||||
|
||||
if (osInfo.Name != null && PlatformInfo.IsMono)
|
||||
{
|
||||
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
|
||||
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
|
||||
scope.Contexts.OperatingSystem.RawDescription = osInfo.FullName;
|
||||
scope.Contexts.OperatingSystem.Version = osInfo.Version.ToString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,7 +206,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
if (ex != null)
|
||||
{
|
||||
fingerPrint.Add(ex.GetType().FullName);
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
if (ex.TargetSite != null)
|
||||
{
|
||||
fingerPrint.Add(ex.TargetSite.ToString());
|
||||
}
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||
|
||||
@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
|
||||
try
|
||||
{
|
||||
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
|
||||
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
|
||||
|
||||
var key = environmentVariable.Key.ToString();
|
||||
var value = environmentVariable.Value?.ToString();
|
||||
|
||||
if (startInfo.EnvironmentVariables.ContainsKey(key))
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.EnvironmentVariables.Add(key, value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -366,11 +377,6 @@ namespace NzbDrone.Common.Processes
|
||||
|
||||
private (string Path, string Args) GetPathAndArgs(string path, string args)
|
||||
{
|
||||
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("mono", $"--debug {path} {args}");
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("cmd.exe", $"/c {path} {args}");
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
public void SingleOrDefault_should_return_null_on_empty_db()
|
||||
{
|
||||
Mocker.Resolve<IDatabase>()
|
||||
.OpenConnection().Query<Author>("SELECT * FROM Authors")
|
||||
.OpenConnection().Query<Author>("SELECT * FROM \"Authors\"")
|
||||
.SingleOrDefault(c => c.CleanName == "SomeTitle")
|
||||
.Should()
|
||||
.BeNull();
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
public void should_lazy_load_author_for_trackfile()
|
||||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var tracks = db.Query<BookFile>(new SqlBuilder()).ToList();
|
||||
var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||
|
||||
Assert.IsNotEmpty(tracks);
|
||||
foreach (var track in tracks)
|
||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
public void should_lazy_load_trackfile_if_not_joined()
|
||||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var tracks = db.Query<Book>(new SqlBuilder()).ToList();
|
||||
var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||
|
||||
foreach (var track in tracks)
|
||||
{
|
||||
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var files = MediaFileRepository.Query(db,
|
||||
new SqlBuilder()
|
||||
new SqlBuilder(db.DatabaseType)
|
||||
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
|
||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||
|
||||
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class WhereBuilderPostgresFixture : CoreTest
|
||||
{
|
||||
private WhereBuilderPostgres _subject;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void MapTables()
|
||||
{
|
||||
// Generate table mapping
|
||||
Mocker.Resolve<DbFactory>();
|
||||
}
|
||||
|
||||
private WhereBuilderPostgres Where(Expression<Func<Author, bool>> filter)
|
||||
{
|
||||
return new WhereBuilderPostgres(filter, true, 0);
|
||||
}
|
||||
|
||||
private WhereBuilderPostgres WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
{
|
||||
return new WhereBuilderPostgres(filter, true, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_const()
|
||||
{
|
||||
_subject = Where(x => x.Id == 10);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_variable()
|
||||
{
|
||||
var id = 10;
|
||||
_subject = Where(x => x.Id == id);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_property()
|
||||
{
|
||||
var author = new Author { Id = 10 };
|
||||
_subject = Where(x => x.Id == author.Id);
|
||||
|
||||
_subject.Parameters.ParameterNames.Should().HaveCount(1);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(author.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_joined_property()
|
||||
{
|
||||
_subject = Where(x => x.QualityProfile.Value.Id == 1);
|
||||
|
||||
_subject.Parameters.ParameterNames.Should().HaveCount(1);
|
||||
_subject.ToString().Should().Be($"(\"QualityProfiles\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilderPostgres(filter, true, 0);
|
||||
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilderPostgres(filter, false, 0);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_is_null()
|
||||
{
|
||||
_subject = Where(x => x.CleanName == null);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_is_null_value()
|
||||
{
|
||||
string cleanName = null;
|
||||
_subject = Where(x => x.CleanName == cleanName);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_null_property()
|
||||
{
|
||||
var author = new Author { CleanName = null };
|
||||
_subject = Where(x => x.CleanName == author.CleanName);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_contains_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.Contains(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_contains_column()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => test.Contains(x.CleanName));
|
||||
|
||||
_subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Authors\".\"CleanName\" || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_starts_with_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.StartsWith(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE @Clause1_P1 || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_ends_with_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.EndsWith(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1)");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_in_list()
|
||||
{
|
||||
var list = new List<int> { 1, 2, 3 };
|
||||
_subject = Where(x => list.Contains(x.Id));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}')))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_in_list_2()
|
||||
{
|
||||
var list = new List<int> { 1, 2, 3 };
|
||||
_subject = Where(x => x.CleanName == "test" && list.Contains(x.Id));
|
||||
|
||||
_subject.ToString().Should().Be($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}'))))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_in_string_list()
|
||||
{
|
||||
var list = new List<string> { "first", "second", "third" };
|
||||
|
||||
_subject = Where(x => list.Contains(x.CleanName));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_as_int()
|
||||
{
|
||||
_subject = WhereMetadata(x => x.Status == AuthorStatusType.Continuing);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_in_list()
|
||||
{
|
||||
var allowed = new List<AuthorStatusType> { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_in_array()
|
||||
{
|
||||
var allowed = new AuthorStatusType[] { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class WhereBuilderFixture : CoreTest
|
||||
public class WhereBuilderSqliteFixture : CoreTest
|
||||
{
|
||||
private WhereBuilder _subject;
|
||||
private WhereBuilderSqlite _subject;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void MapTables()
|
||||
@@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
Mocker.Resolve<DbFactory>();
|
||||
}
|
||||
|
||||
private WhereBuilder Where(Expression<Func<Author, bool>> filter)
|
||||
private WhereBuilderSqlite Where(Expression<Func<Author, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(filter, true, 0);
|
||||
}
|
||||
|
||||
private WhereBuilder WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
private WhereBuilderSqlite WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(filter, true, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilder(filter, true, 0);
|
||||
_subject = new WhereBuilderSqlite(filter, true, 0);
|
||||
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilder(filter, false, 0);
|
||||
_subject = new WhereBuilderSqlite(filter, false, 0);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
@@ -5,6 +6,7 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -244,5 +246,89 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repacks_are_not_preferred()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotPrefer);
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "";
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_repack_but_auto_download_repacks_is_true()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.PreferAndUpgrade);
|
||||
|
||||
_parsedBookInfo.Quality.Revision.IsRepack = true;
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "Readarr";
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_when_repack_but_auto_download_repacks_is_false()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(s => s.DownloadPropersAndRepacks)
|
||||
.Returns(ProperDownloadTypes.DoNotUpgrade);
|
||||
|
||||
_parsedBookInfo.Quality.Revision.IsRepack = true;
|
||||
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.ReleaseGroup = "Readarr";
|
||||
return c;
|
||||
}).ToList();
|
||||
_trackFiles.Select(c =>
|
||||
{
|
||||
c.Quality = new QualityModel(Quality.FLAC);
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
var remoteAlbum = Builder<RemoteBook>.CreateNew()
|
||||
.With(e => e.ParsedBookInfo = _parsedBookInfo)
|
||||
.With(e => e.Books = _books)
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.FreeboxDownloadTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TorrentFreeboxDownloadFixture : DownloadClientFixtureBase<TorrentFreeboxDownload>
|
||||
{
|
||||
protected FreeboxDownloadSettings _settings;
|
||||
|
||||
protected FreeboxDownloadConfiguration _downloadConfiguration;
|
||||
|
||||
protected FreeboxDownloadTask _task;
|
||||
|
||||
protected string _defaultDestination = @"/some/path";
|
||||
protected string _encodedDefaultDestination = "L3NvbWUvcGF0aA==";
|
||||
protected string _category = "somecat";
|
||||
protected string _encodedDefaultDestinationAndCategory = "L3NvbWUvcGF0aC9zb21lY2F0";
|
||||
protected string _destinationDirectory = @"/path/to/media";
|
||||
protected string _encodedDestinationDirectory = "L3BhdGgvdG8vbWVkaWE=";
|
||||
protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata");
|
||||
protected string _downloadURL => "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new DownloadClientDefinition();
|
||||
|
||||
_settings = new FreeboxDownloadSettings()
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 443,
|
||||
ApiUrl = "/api/v1/",
|
||||
AppId = "someid",
|
||||
AppToken = "S0mEv3RY1oN9T0k3n"
|
||||
};
|
||||
|
||||
Subject.Definition.Settings = _settings;
|
||||
|
||||
_downloadConfiguration = new FreeboxDownloadConfiguration()
|
||||
{
|
||||
DownloadDirectory = _encodedDefaultDestination
|
||||
};
|
||||
|
||||
_task = new FreeboxDownloadTask()
|
||||
{
|
||||
Id = "id0",
|
||||
Name = "name",
|
||||
DownloadDirectory = "L3NvbWUvcGF0aA==",
|
||||
InfoHash = "HASH",
|
||||
QueuePosition = 1,
|
||||
Status = FreeboxDownloadTaskStatus.Unknown,
|
||||
Eta = 0,
|
||||
Error = "none",
|
||||
Type = FreeboxDownloadTaskType.Bt.ToString(),
|
||||
IoPriority = FreeboxDownloadTaskIoPriority.Normal.ToString(),
|
||||
StopRatio = 150,
|
||||
PieceLength = 125,
|
||||
CreatedTimestamp = 1665261599,
|
||||
Size = 1000,
|
||||
ReceivedPrct = 0,
|
||||
ReceivedBytes = 0,
|
||||
ReceivedRate = 0,
|
||||
TransmittedPrct = 0,
|
||||
TransmittedBytes = 0,
|
||||
TransmittedRate = 0,
|
||||
};
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
|
||||
}
|
||||
|
||||
protected void GivenCategory()
|
||||
{
|
||||
_settings.Category = _category;
|
||||
}
|
||||
|
||||
protected void GivenDestinationDirectory()
|
||||
{
|
||||
_settings.DestinationDirectory = _destinationDirectory;
|
||||
}
|
||||
|
||||
protected virtual void GivenDownloadConfiguration()
|
||||
{
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Setup(s => s.GetDownloadConfiguration(It.IsAny<FreeboxDownloadSettings>()))
|
||||
.Returns(_downloadConfiguration);
|
||||
}
|
||||
|
||||
protected virtual void GivenTasks(List<FreeboxDownloadTask> torrents)
|
||||
{
|
||||
if (torrents == null)
|
||||
{
|
||||
torrents = new List<FreeboxDownloadTask>();
|
||||
}
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Setup(s => s.GetTasks(It.IsAny<FreeboxDownloadSettings>()))
|
||||
.Returns(torrents);
|
||||
}
|
||||
|
||||
protected void PrepareClientToReturnQueuedItem()
|
||||
{
|
||||
_task.Status = FreeboxDownloadTaskStatus.Queued;
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask>
|
||||
{
|
||||
_task
|
||||
});
|
||||
}
|
||||
|
||||
protected void GivenSuccessfulDownload()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Setup(s => s.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()))
|
||||
.Callback(PrepareClientToReturnQueuedItem);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Setup(s => s.AddTaskFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()))
|
||||
.Callback(PrepareClientToReturnQueuedItem);
|
||||
}
|
||||
|
||||
protected override RemoteEpisode CreateRemoteEpisode()
|
||||
{
|
||||
var episode = base.CreateRemoteEpisode();
|
||||
|
||||
episode.Release.DownloadUrl = _downloadURL;
|
||||
|
||||
return episode;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_with_DestinationDirectory_should_force_directory()
|
||||
{
|
||||
GivenDestinationDirectory();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDestinationDirectory, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_with_Category_should_force_directory()
|
||||
{
|
||||
GivenDownloadConfiguration();
|
||||
GivenCategory();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDefaultDestinationAndCategory, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_without_DestinationDirectory_and_Category_should_use_default()
|
||||
{
|
||||
GivenDownloadConfiguration();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _encodedDefaultDestination, It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[TestCase(false, false)]
|
||||
[TestCase(true, true)]
|
||||
public void Download_should_pause_torrent_as_expected(bool addPausedSetting, bool toBePausedFlag)
|
||||
{
|
||||
_settings.AddPaused = addPausedSetting;
|
||||
|
||||
GivenDownloadConfiguration();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), toBePausedFlag, It.IsAny<bool>(), It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[TestCase(0, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.First, true)]
|
||||
[TestCase(0, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.First, true)]
|
||||
[TestCase(0, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.Last, false)]
|
||||
[TestCase(0, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.Last, false)]
|
||||
[TestCase(15, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.First, true)]
|
||||
[TestCase(15, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.First, false)]
|
||||
[TestCase(15, (int)FreeboxDownloadPriority.First, (int)FreeboxDownloadPriority.Last, true)]
|
||||
[TestCase(15, (int)FreeboxDownloadPriority.Last, (int)FreeboxDownloadPriority.Last, false)]
|
||||
public void Download_should_queue_torrent_first_as_expected(int ageDay, int olderPriority, int recentPriority, bool toBeQueuedFirstFlag)
|
||||
{
|
||||
_settings.OlderPriority = olderPriority;
|
||||
_settings.RecentPriority = recentPriority;
|
||||
|
||||
GivenDownloadConfiguration();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
var episode = new Tv.Episode()
|
||||
{
|
||||
AirDateUtc = DateTime.UtcNow.Date.AddDays(-ageDay)
|
||||
};
|
||||
|
||||
remoteEpisode.Episodes.Add(episode);
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), toBeQueuedFirstFlag, It.IsAny<double?>(), It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[TestCase(0, 0)]
|
||||
[TestCase(1.5, 150)]
|
||||
public void Download_should_define_seed_ratio_as_expected(double? providerSeedRatio, double? expectedSeedRatio)
|
||||
{
|
||||
GivenDownloadConfiguration();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
remoteEpisode.SeedConfiguration = new TorrentSeedConfiguration();
|
||||
remoteEpisode.SeedConfiguration.Ratio = providerSeedRatio;
|
||||
|
||||
Subject.Download(remoteEpisode);
|
||||
|
||||
Mocker.GetMock<IFreeboxDownloadProxy>()
|
||||
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>(), expectedSeedRatio, It.IsAny<FreeboxDownloadSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_return_empty_list_if_no_tasks_available()
|
||||
{
|
||||
GivenTasks(new List<FreeboxDownloadTask>());
|
||||
|
||||
Subject.GetItems().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_return_ignore_tasks_of_unknown_type()
|
||||
{
|
||||
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||
_task.Type = "toto";
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||
|
||||
Subject.GetItems().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_when_destinationdirectory_is_set_should_ignore_downloads_in_wrong_folder()
|
||||
{
|
||||
_settings.DestinationDirectory = @"/some/path/that/will/not/match";
|
||||
|
||||
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||
|
||||
Subject.GetItems().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_when_category_is_set_should_ignore_downloads_in_wrong_folder()
|
||||
{
|
||||
_settings.Category = "somecategory";
|
||||
|
||||
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||
|
||||
Subject.GetItems().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[TestCase(FreeboxDownloadTaskStatus.Downloading, false, false)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Done, true, true)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Seeding, false, false)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Stopped, false, false)]
|
||||
public void GetItems_should_return_canBeMoved_and_canBeDeleted_as_expected(FreeboxDownloadTaskStatus apiStatus, bool canMoveFilesExpected, bool canBeRemovedExpected)
|
||||
{
|
||||
_task.Status = apiStatus;
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask>() { _task });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().CanBeRemoved.Should().Be(canBeRemovedExpected);
|
||||
items.First().CanMoveFiles.Should().Be(canMoveFilesExpected);
|
||||
}
|
||||
|
||||
[TestCase(FreeboxDownloadTaskStatus.Stopped, DownloadItemStatus.Paused)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Stopping, DownloadItemStatus.Paused)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Queued, DownloadItemStatus.Queued)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Starting, DownloadItemStatus.Downloading)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Retry, DownloadItemStatus.Downloading)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Checking, DownloadItemStatus.Downloading)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Error, DownloadItemStatus.Warning)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Seeding, DownloadItemStatus.Completed)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Done, DownloadItemStatus.Completed)]
|
||||
[TestCase(FreeboxDownloadTaskStatus.Unknown, DownloadItemStatus.Downloading)]
|
||||
public void GetItems_should_return_item_as_downloadItemStatus(FreeboxDownloadTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||
{
|
||||
_task.Status = apiStatus;
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask>() { _task });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Status.Should().Be(expectedItemStatus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_return_decoded_destination_directory()
|
||||
{
|
||||
var decodedDownloadDirectory = "/that/the/path";
|
||||
|
||||
_task.Status = FreeboxDownloadTaskStatus.Done;
|
||||
_task.DownloadDirectory = "L3RoYXQvdGhlL3BhdGg=";
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().OutputPath.Should().Be(decodedDownloadDirectory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_return_message_if_tasks_in_error()
|
||||
{
|
||||
_task.Status = FreeboxDownloadTaskStatus.Error;
|
||||
_task.Error = "internal";
|
||||
|
||||
GivenTasks(new List<FreeboxDownloadTask> { _task });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Message.Should().Be("Internal error.");
|
||||
items.First().Status.Should().Be(DownloadItemStatus.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Test.Common.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
@@ -47,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
public abstract class DbTest : CoreTest
|
||||
{
|
||||
private ITestDatabase _db;
|
||||
private DatabaseType _databaseType;
|
||||
|
||||
protected virtual MigrationType MigrationType => MigrationType.Main;
|
||||
|
||||
@@ -65,8 +72,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
|
||||
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
||||
{
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
var database = factory.Create(migrationContext);
|
||||
var database = CreateDatabase(migrationContext);
|
||||
Mocker.SetConstant(database);
|
||||
|
||||
switch (MigrationType)
|
||||
@@ -98,6 +104,65 @@ namespace NzbDrone.Core.Test.Framework
|
||||
return testDb;
|
||||
}
|
||||
|
||||
private IDatabase CreateDatabase(MigrationContext migrationContext)
|
||||
{
|
||||
if (_databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
CreatePostgresDb();
|
||||
}
|
||||
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
|
||||
// If a special migration test or log migration then create new
|
||||
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
return factory.Create(migrationContext);
|
||||
}
|
||||
|
||||
return CreateSqliteDatabase(factory, migrationContext);
|
||||
}
|
||||
|
||||
private void CreatePostgresDb()
|
||||
{
|
||||
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
|
||||
PostgresDatabase.Create(options, MigrationType);
|
||||
}
|
||||
|
||||
private void DropPostgresDb()
|
||||
{
|
||||
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
|
||||
PostgresDatabase.Drop(options, MigrationType);
|
||||
}
|
||||
|
||||
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
|
||||
{
|
||||
// Otherwise try to use a cached migrated db
|
||||
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
|
||||
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
|
||||
if (File.Exists(cachedDb))
|
||||
{
|
||||
TestLogger.Info($"Using cached initial database {cachedDb}");
|
||||
File.Copy(cachedDb, testDb);
|
||||
return factory.Create(migrationContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = factory.Create(migrationContext);
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
SQLiteConnection.ClearAllPools();
|
||||
|
||||
TestLogger.Info("Caching database");
|
||||
File.Copy(testDb, cachedDb);
|
||||
return db;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTestSqliteDb(MigrationType type)
|
||||
{
|
||||
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
|
||||
}
|
||||
|
||||
protected virtual void SetupLogging()
|
||||
{
|
||||
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
|
||||
@@ -108,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
|
||||
WithTempAsAppPath();
|
||||
SetupLogging();
|
||||
|
||||
// populate the possible postgres options
|
||||
var postgresOptions = PostgresDatabase.GetTestOptions();
|
||||
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
|
||||
|
||||
// Set up remaining container services
|
||||
Mocker.SetConstant(Options.Create(postgresOptions));
|
||||
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
|
||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||
|
||||
@@ -127,12 +199,19 @@ namespace NzbDrone.Core.Test.Framework
|
||||
// Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
SQLiteConnection.ClearAllPools();
|
||||
NpgsqlConnection.ClearAllPools();
|
||||
|
||||
if (TestFolderInfo != null)
|
||||
{
|
||||
DeleteTempFolder(TestFolderInfo.AppDataFolder);
|
||||
}
|
||||
|
||||
if (_databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
DropPostgresDb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using Unity.Resolution;
|
||||
using NzbDrone.Test.Common.AutoMoq;
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
@@ -14,12 +15,9 @@ namespace NzbDrone.Core.Test.Framework
|
||||
[SetUp]
|
||||
public void FileSystemTestSetup()
|
||||
{
|
||||
FileSystem = new MockFileSystem();
|
||||
FileSystem = (MockFileSystem)Mocker.Resolve<IFileSystem>(FileSystemType.Mock);
|
||||
|
||||
DiskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider", new ResolverOverride[]
|
||||
{
|
||||
new ParameterOverride("fileSystem", FileSystem)
|
||||
});
|
||||
DiskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Mock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
where T : ModelBase, new();
|
||||
IDirectDataMapper GetDirectDataMapper();
|
||||
IDbConnection OpenConnection();
|
||||
DatabaseType DatabaseType { get; }
|
||||
}
|
||||
|
||||
public class TestDatabase : ITestDatabase
|
||||
@@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
|
||||
private readonly IDatabase _dbConnection;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
|
||||
|
||||
public TestDatabase(IDatabase dbConnection)
|
||||
{
|
||||
_eventAggregator = new Mock<IEventAggregator>().Object;
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
.Setup(s => s.GetAuthor(It.IsAny<int>()))
|
||||
.Returns(_author);
|
||||
|
||||
Mocker.GetMock<ISearchForNzb>()
|
||||
Mocker.GetMock<ISearchForReleases>()
|
||||
.Setup(s => s.AuthorSearch(_author.Id, false, true, false))
|
||||
.Returns(new List<DownloadDecision>());
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||
|
||||
Subject.Execute(new AuthorSearchCommand { AuthorId = _author.Id, Trigger = CommandTrigger.Manual });
|
||||
|
||||
Mocker.GetMock<ISearchForNzb>()
|
||||
Mocker.GetMock<ISearchForReleases>()
|
||||
.Verify(v => v.AuthorSearch(_author.Id, false, true, false),
|
||||
Times.Exactly(_author.Books.Value.Count(s => s.Monitored)));
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
if (PlatformInfo.IsMono && PlatformInfo.GetVersion() < new Version(5, 8))
|
||||
{
|
||||
Assert.Inconclusive("Not supported on Mono < 5.8");
|
||||
}
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.FileExists(It.IsAny<string>()))
|
||||
.Returns<string>(s => File.Exists(s));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
@@ -133,6 +135,13 @@ namespace NzbDrone.Core.Test.MediaCoverTests
|
||||
}
|
||||
};
|
||||
|
||||
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "NonExistant.mp4");
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(c => c.GetFileInfo(It.IsAny<string>()))
|
||||
.Returns((FileInfoBase)fileInfo);
|
||||
|
||||
Subject.ConvertToLocalUrls(12, MediaCoverEntity.Author, covers);
|
||||
|
||||
covers.Single().Url.Should().Be("/MediaCover/12/banner" + extension);
|
||||
|
||||
@@ -13,6 +13,7 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
using NzbDrone.Test.Common.AutoMoq;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||
{
|
||||
@@ -55,9 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_diskProvider = Mocker.Resolve<IDiskProvider>("ActualDiskProvider");
|
||||
|
||||
Mocker.SetConstant<IDiskProvider>(_diskProvider);
|
||||
_diskProvider = Mocker.Resolve<IDiskProvider>(FileSystemType.Actual);
|
||||
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.Setup(x => x.WriteAudioTags)
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
}
|
||||
|
||||
[TestCase("Harry Potter and the sorcerer's stone", 3)]
|
||||
[TestCase("B0192CTMYG", 42844155)]
|
||||
[TestCase("B0192CTMYG", 61209488)]
|
||||
[TestCase("9780439554930", 48517161)]
|
||||
public void successful_book_search(string title, int expected)
|
||||
{
|
||||
|
||||
@@ -3,8 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Equivalency;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
@@ -21,6 +23,13 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
AssertionOptions.AssertEquivalencyUsing(options =>
|
||||
{
|
||||
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
|
||||
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
|
||||
return options;
|
||||
});
|
||||
|
||||
_author = new Author
|
||||
{
|
||||
Name = "Alien Ant Farm",
|
||||
@@ -143,7 +152,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -152,7 +161,11 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||
.Excluding(x => x.AuthorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Npgsql;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -145,7 +147,14 @@ namespace NzbDrone.Core.Test.MusicTests.AuthorRepositoryTests
|
||||
_authorRepo.Insert(author1);
|
||||
|
||||
Action insertDupe = () => _authorRepo.Insert(author2);
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
if (Db.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
insertDupe.Should().Throw<PostgresException>();
|
||||
}
|
||||
else
|
||||
{
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
||||
private void GivenBooksForRefresh(List<Book> books)
|
||||
{
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||
.Returns(books);
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
||||
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.InSequence(seq)
|
||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||
.Returns(new List<Book>());
|
||||
|
||||
// Update called twice for a move/merge
|
||||
@@ -298,7 +298,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
||||
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.InSequence(seq)
|
||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>()))
|
||||
.Returns(_books);
|
||||
|
||||
// Update called twice for a move/merge
|
||||
|
||||
@@ -7,7 +7,6 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class QualityParserFixture : CoreTest
|
||||
{
|
||||
public static object[] SelfQualityParserCases =
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
|
||||
.Returns(true);
|
||||
|
||||
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
|
||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
public void should_return_with_warning_if_updater_doesnt_exists()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update.exe"))))
|
||||
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Readarr.Update".ProcessNameToExe()))))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
|
||||
|
||||
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
|
||||
{
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
|
||||
private readonly IMainDatabase _database;
|
||||
|
||||
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
|
||||
}
|
||||
}
|
||||
|
||||
private SqlBuilder Builder() => new SqlBuilder()
|
||||
.Select(@"Authors.Id AS AuthorId,
|
||||
Books.Id AS BookId,
|
||||
SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk,
|
||||
1 AS TotalBookCount,
|
||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount,
|
||||
CASE WHEN (Books.Monitored = 1 AND (Books.ReleaseDate < @currentDate) OR Books.ReleaseDate IS NULL) OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END AS BookCount,
|
||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount")
|
||||
private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Select(@"""Authors"".""Id"" AS ""AuthorId"",
|
||||
""Books"".""Id"" AS ""BookId"",
|
||||
SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"",
|
||||
1 AS ""TotalBookCount"",
|
||||
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE 1 END AS ""AvailableBookCount"",
|
||||
CASE WHEN (""Books"".""Monitored"" = true AND (""Books"".""ReleaseDate"" < @currentDate) OR ""Books"".""ReleaseDate"" IS NULL) OR MIN(""BookFiles"".""Id"") IS NOT NULL THEN 1 ELSE 0 END AS ""BookCount"",
|
||||
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""")
|
||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
|
||||
|
||||
@@ -15,12 +15,14 @@ namespace NzbDrone.Core.AuthorStats
|
||||
}
|
||||
|
||||
public class AuthorStatisticsService : IAuthorStatisticsService,
|
||||
IHandle<AuthorAddedEvent>,
|
||||
IHandle<AuthorUpdatedEvent>,
|
||||
IHandle<AuthorDeletedEvent>,
|
||||
IHandle<BookAddedEvent>,
|
||||
IHandle<BookDeletedEvent>,
|
||||
IHandle<BookImportedEvent>,
|
||||
IHandle<BookEditedEvent>,
|
||||
IHandle<BookUpdatedEvent>,
|
||||
IHandle<BookFileDeletedEvent>
|
||||
{
|
||||
private readonly IAuthorStatisticsRepository _authorStatisticsRepository;
|
||||
@@ -68,6 +70,13 @@ namespace NzbDrone.Core.AuthorStats
|
||||
return authorStatistics;
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(AuthorAddedEvent message)
|
||||
{
|
||||
_cache.Remove("AllAuthors");
|
||||
_cache.Remove(message.Author.Id.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(AuthorUpdatedEvent message)
|
||||
{
|
||||
@@ -110,6 +119,13 @@ namespace NzbDrone.Core.AuthorStats
|
||||
_cache.Remove(message.Book.AuthorId.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(BookUpdatedEvent message)
|
||||
{
|
||||
_cache.Remove("AllAuthors");
|
||||
_cache.Remove(message.Book.AuthorId.ToString());
|
||||
}
|
||||
|
||||
[EventHandleOrder(EventHandleOrder.First)]
|
||||
public void Handle(BookFileDeletedEvent message)
|
||||
{
|
||||
|
||||
@@ -183,9 +183,12 @@ namespace NzbDrone.Core.Backup
|
||||
|
||||
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()
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Blocklisting
|
||||
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<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
|
||||
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);
|
||||
|
||||
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
|
||||
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Books
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Books
|
||||
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
|
||||
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);
|
||||
Book FindByTitle(int authorMetadataId, string title);
|
||||
Book FindById(string foreignBookId);
|
||||
@@ -44,17 +44,35 @@ namespace NzbDrone.Core.Books
|
||||
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.Having("Books.ReleaseDate = MAX(Books.ReleaseDate)"));
|
||||
|
||||
var inner = Builder()
|
||||
.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)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.Having("Books.ReleaseDate = MIN(Books.ReleaseDate)"));
|
||||
|
||||
var inner = Builder()
|
||||
.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)
|
||||
@@ -62,14 +80,14 @@ namespace NzbDrone.Core.Books
|
||||
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));
|
||||
}
|
||||
|
||||
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<Edition, BookFile>((l, r) => l.Id == r.EditionId)
|
||||
.Where<BookFile>(f => fileIds.Contains(f.Id)))
|
||||
@@ -125,7 +143,7 @@ namespace NzbDrone.Core.Books
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
return pagingSpec;
|
||||
|
||||
@@ -11,11 +11,11 @@ namespace NzbDrone.Core.Books
|
||||
{
|
||||
List<Edition> GetAllMonitoredEditions();
|
||||
Edition FindByForeignEditionId(string foreignEditionId);
|
||||
List<Edition> FindByBook(int id);
|
||||
List<Edition> FindByBook(IEnumerable<int> ids);
|
||||
List<Edition> FindByAuthor(int id);
|
||||
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ namespace NzbDrone.Core.Books
|
||||
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));
|
||||
}
|
||||
|
||||
public List<Edition> FindByBook(int id)
|
||||
public List<Edition> FindByBook(IEnumerable<int> ids)
|
||||
{
|
||||
// populate the books and author metadata also
|
||||
// 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<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) =>
|
||||
{
|
||||
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Books
|
||||
|
||||
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);
|
||||
Ensure.That(allEditions.Count(x => x.Monitored) == 1).IsTrue();
|
||||
UpdateMany(allEditions);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Books
|
||||
public interface ISeriesRepository : IBasicRepository<Series>
|
||||
{
|
||||
Series FindById(string foreignSeriesId);
|
||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
||||
List<Series> FindById(List<string> foreignSeriesId);
|
||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||
List<Series> GetByAuthorId(int authorId);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Books
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Books
|
||||
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||
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);
|
||||
Book AddBook(Book newBook, bool doRefresh = true);
|
||||
Book FindById(string foreignId);
|
||||
@@ -206,7 +206,7 @@ namespace NzbDrone.Core.Books
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ namespace NzbDrone.Core.Books
|
||||
void InsertMany(List<Edition> editions);
|
||||
void UpdateMany(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(IEnumerable<int> bookIds);
|
||||
List<Edition> GetEditionsByAuthor(int authorId);
|
||||
Edition FindByTitle(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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Books
|
||||
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
@@ -30,6 +31,7 @@ namespace NzbDrone.Core.Books
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
private readonly IAddAuthorService _addAuthorService;
|
||||
private readonly IEditionService _editionService;
|
||||
private readonly IProvideAuthorInfo _authorInfo;
|
||||
@@ -44,6 +46,7 @@ namespace NzbDrone.Core.Books
|
||||
|
||||
public RefreshBookService(IBookService bookService,
|
||||
IAuthorService authorService,
|
||||
IRootFolderService rootFolderService,
|
||||
IAddAuthorService addAuthorService,
|
||||
IEditionService editionService,
|
||||
IAuthorMetadataService authorMetadataService,
|
||||
@@ -60,6 +63,7 @@ namespace NzbDrone.Core.Books
|
||||
{
|
||||
_bookService = bookService;
|
||||
_authorService = authorService;
|
||||
_rootFolderService = rootFolderService;
|
||||
_addAuthorService = addAuthorService;
|
||||
_editionService = editionService;
|
||||
_authorInfo = authorInfo;
|
||||
@@ -142,7 +146,7 @@ namespace NzbDrone.Core.Books
|
||||
Metadata = remote.AuthorMetadata.Value,
|
||||
MetadataProfileId = oldAuthor.MetadataProfileId,
|
||||
QualityProfileId = oldAuthor.QualityProfileId,
|
||||
RootFolderPath = oldAuthor.RootFolderPath,
|
||||
RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldAuthor.Path),
|
||||
Monitored = oldAuthor.Monitored,
|
||||
Tags = oldAuthor.Tags
|
||||
};
|
||||
@@ -246,7 +250,7 @@ namespace NzbDrone.Core.Books
|
||||
|
||||
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)
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace NzbDrone.Core.Books
|
||||
var updated = false;
|
||||
|
||||
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 books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NzbDrone.Core.Books
|
||||
public interface ISeriesService
|
||||
{
|
||||
Series FindById(string foreignSeriesId);
|
||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
||||
List<Series> FindById(List<string> foreignSeriesId);
|
||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||
List<Series> GetByAuthorId(int authorId);
|
||||
void Delete(int seriesId);
|
||||
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.Books
|
||||
return _seriesRepository.FindById(foreignSeriesId);
|
||||
}
|
||||
|
||||
public List<Series> FindById(IEnumerable<string> foreignSeriesId)
|
||||
public List<Series> FindById(List<string> foreignSeriesId)
|
||||
{
|
||||
return _seriesRepository.FindById(foreignSeriesId);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -18,7 +20,7 @@ using NzbDrone.Core.Update;
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||
IExecute<ResetApiKeyCommand>
|
||||
IExecute<ResetApiKeyCommand>
|
||||
{
|
||||
Dictionary<string, object> GetConfigDictionary();
|
||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||
@@ -48,6 +50,13 @@ namespace NzbDrone.Core.Configuration
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
string PostgresCacheDb { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -57,6 +66,7 @@ namespace NzbDrone.Core.Configuration
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly ICached<string> _cache;
|
||||
private readonly PostgresOptions _postgresOptions;
|
||||
|
||||
private readonly string _configFile;
|
||||
|
||||
@@ -65,12 +75,14 @@ namespace NzbDrone.Core.Configuration
|
||||
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
|
||||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator,
|
||||
IDiskProvider diskProvider)
|
||||
IDiskProvider diskProvider,
|
||||
IOptions<PostgresOptions> postgresOptions)
|
||||
{
|
||||
_cache = cacheManager.GetCache<string>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_diskProvider = diskProvider;
|
||||
_configFile = appFolderInfo.GetConfigPath();
|
||||
_postgresOptions = postgresOptions.Value;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfigDictionary()
|
||||
@@ -184,6 +196,13 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info");
|
||||
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 int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
|
||||
@@ -410,6 +410,8 @@ namespace NzbDrone.Core.Configuration
|
||||
public CertificateValidationType CertificateValidation =>
|
||||
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
|
||||
|
||||
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
|
||||
|
||||
private string GetValue(string key)
|
||||
{
|
||||
return GetValue(key, string.Empty);
|
||||
|
||||
@@ -97,5 +97,6 @@ namespace NzbDrone.Core.Configuration
|
||||
int BackupRetention { get; }
|
||||
|
||||
CertificateValidationType CertificateValidation { get; }
|
||||
string ApplicationUrl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Datastore
|
||||
_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();
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -194,7 +199,8 @@ namespace NzbDrone.Core.Datastore
|
||||
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);
|
||||
|
||||
return model;
|
||||
@@ -305,7 +311,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
conn.Execute($"DELETE FROM [{_table}]");
|
||||
conn.Execute($"DELETE FROM \"{_table}\"");
|
||||
}
|
||||
|
||||
if (vacuum)
|
||||
@@ -364,7 +370,7 @@ namespace NzbDrone.Core.Datastore
|
||||
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("UPDATE {0} SET ", _table);
|
||||
sb.AppendFormat("UPDATE \"{0}\" SET ", _table);
|
||||
|
||||
for (var i = 0; i < propertiesToUpdate.Count; i++)
|
||||
{
|
||||
@@ -446,9 +452,10 @@ namespace NzbDrone.Core.Datastore
|
||||
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
||||
}
|
||||
|
||||
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
|
||||
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
||||
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
return queryFunc(builder).ToList();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
|
||||
public class CacheDatabase : ICacheDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public CacheDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
@@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
@@ -15,11 +17,20 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public class ConnectionStringFactory : IConnectionStringFactory
|
||||
{
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
|
||||
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
CacheDbConnectionString = GetConnectionString(appFolderInfo.GetCacheDatabase());
|
||||
_configFileProvider = configFileProvider;
|
||||
|
||||
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; }
|
||||
@@ -51,5 +62,19 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.Datastore
|
||||
IDbConnection OpenConnection();
|
||||
Version Version { get; }
|
||||
int Migration { get; }
|
||||
DatabaseType DatabaseType { get; }
|
||||
void Vacuum();
|
||||
}
|
||||
|
||||
@@ -32,13 +34,44 @@ namespace NzbDrone.Core.Datastore
|
||||
return _datamapperFactory();
|
||||
}
|
||||
|
||||
public DatabaseType DatabaseType
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version Version
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
var version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
string version;
|
||||
|
||||
try
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SHOW server_version");
|
||||
|
||||
//Postgres can return extra info about operating system on version call, ignore this
|
||||
version = Regex.Replace(version, @"\(.*?\)", "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
}
|
||||
|
||||
return new Version(version);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +83,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
return db.QueryFirstOrDefault<int>("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1");
|
||||
return db.QueryFirstOrDefault<int>("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,4 +106,10 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DatabaseType
|
||||
{
|
||||
SQLite,
|
||||
PostgreSQL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Sockets;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
@@ -92,10 +95,19 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
var db = new Database(migrationContext.MigrationType.ToString(), () =>
|
||||
{
|
||||
var conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
conn.Open();
|
||||
DbConnection conn;
|
||||
|
||||
if (connectionString.Contains(".db"))
|
||||
{
|
||||
conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn = new NpgsqlConnection(connectionString);
|
||||
}
|
||||
|
||||
conn.Open();
|
||||
return conn;
|
||||
});
|
||||
|
||||
@@ -120,6 +132,37 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/readarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
|
||||
}
|
||||
catch (NpgsqlException e)
|
||||
{
|
||||
if (e.InnerException is SocketException)
|
||||
{
|
||||
var retryCount = 3;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||
|
||||
try
|
||||
{
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (--retryCount > 0)
|
||||
{
|
||||
System.Threading.Thread.Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ReadarrStartupException(ex, "Error creating main database");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ReadarrStartupException(e, "Error creating main database");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ReadarrStartupException(e, "Error creating main database");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user