Compare commits

...

24 Commits

Author SHA1 Message Date
bakerboy448
3d6023f2f9 Fixed: Improve RarBG Error Handling
(cherry picked from commit 7cd38bba841659a595fe4a0e14c478fc4e4047b1)
2022-10-03 17:20:16 +00:00
Joe Milazzo
14d74f2eca New: Kavita Connection (#1880)
* Added ability for Readarr to inform Kavita when a change occurs for rescan.

* Use the existing API with a POST rather than a new API.

* Updated some wording

* Fixed PR comments
2022-10-02 18:41:38 -05:00
servarr[bot]
c3cbbb7627 Update Bug Report Template [skip ci] (#1842)
(cherry picked from commit 99e0d42b717b279be0f9005039025cd730f90f6c)

Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2022-09-26 21:05:36 -05:00
servarr[bot]
ad3a58c422 New: Parse version with a space before 'v''
* New: Parse anime version with a space before 'v'

(cherry picked from commit e9123982f33ab35ca022f91f345da05fef23d6dc)

* Delete AnimeVersionFixture.cs

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2022-09-26 16:55:34 -05:00
Robin Dadswell
347b154882 Update Bug Report Template
[skip ci]

(cherry picked from commit b9185574f3761e125151907e3d31511689a4513e)
2022-09-26 16:42:52 -05:00
Weblate
63ccc155d6 Translated using Weblate (Russian) [skip ci]
Currently translated at 66.6% (586 of 879 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 60.5% (532 of 879 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.6% (700 of 879 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: emanuelsipos <emanuelsipos1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2022-08-18 07:12:51 -05:00
ta264
76613316fa Fixed: Book filters on author properties
Fixes #1755
2022-08-15 14:04:29 +01:00
ta264
f33e9f2bbc Fixed: Selecting edition for books in manual import 2022-08-15 14:04:29 +01:00
Robin Dadswell
e4a3d7b273 Fixed: Postgres Timezone Issues
Co-authored-by: ta264 <ta264@users.noreply.github.com>
2022-08-13 19:55:48 +01:00
Robin Dadswell
46c2e0ba82 New: Postgres Support
Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com>
Co-authored-by: ta264 <ta264@users.noreply.github.com>

(cherry picked from commit 80b1aa9a2c81617bdda7ef551c19a2f114e49204)
2022-08-13 19:55:48 +01:00
Robin Dadswell
8616373f96 Bump FluentMigrator to 3.3.2 2022-08-13 19:55:48 +01:00
ta264
b3f99d8c20 Fixed: Add missing author when a book's author changes in metadata
Fixes READARR-VH
2022-08-05 11:30:13 +01:00
ta264
c0c0847963 Update packages 2022-08-05 11:30:13 +01:00
Qstick
c92f9d03c0 Fixed: Releases Size filter has incorrect value type
(cherry picked from commit 46bc711558d9f3ab278125a4292eb7851e51d308)
2022-07-29 23:26:22 -04:00
bakerboy448
f7bf1e243d Fixed: List Import additions when book / author lookup required 2022-07-29 12:06:53 -05:00
Weblate
20b1f41ac1 Translated using Weblate (Catalan) [skip ci]
Currently translated at 49.4% (433 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.5% (5 of 876 strings)

Co-authored-by: beefnoodle <acer.wang@protonmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-07-26 09:22:12 +01:00
bakerboy448
f703db1e00 Fixed: Better Cleansing of Tracker Announce Keys
Fixed: Cleanse Notifiarr secret from URL in logs

Fixes: #4623
(cherry picked from commit e6210aede6f7ead197e82572976bc0267d910d46)

(cherry picked from commit ec866082d44d299096112a6c7c232384b1f74505)
2022-07-26 09:21:28 +01:00
bakerboy448
20f67c8035 Fixed: Importing Readarr Lists
Fixes #1747
2022-07-26 09:20:38 +01:00
Qstick
56ae497bfa Fixed: Don't call for server notifications on event driven check 2022-07-17 13:48:12 -05:00
Weblate
e760dc56c6 Translated using Weblate (French) [skip ci]
Currently translated at 75.1% (658 of 876 strings)

Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2022-07-17 13:42:13 -05:00
Weblate
7ad26b386c Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.1% (658 of 876 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 10.9% (96 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.1% (1 of 876 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (669 of 876 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 89.8% (787 of 876 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 66.0% (579 of 876 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.6% (575 of 876 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 62.6% (549 of 876 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (559 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 67.1% (588 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 89.1% (781 of 876 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.0% (587 of 876 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.8% (577 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Added translation using Weblate (Lithuanian) [skip ci]

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: loksum213108 <lok3222003@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-07-13 11:01:02 -05:00
Weblate
199085249f Translated using Weblate (Portuguese) [skip ci]
Currently translated at 75.9% (665 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 88.0% (771 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 64.2% (563 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 87.7% (769 of 876 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Marcin <ml.cichy@gmail.com>
Co-authored-by: Vitor Brito <main@vitorbrito.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2022-06-25 20:30:02 -05:00
ta264
b84041d95f Use DryIoc for Automoqer, drop Unity dependency
(cherry picked from commit e3468daba04b52fbf41ce3004934a26b0220ec4f)
(cherry picked from commit cf4103d73d74896daa12e7a0237e8923eaa6c095)
2022-06-22 10:13:37 +01:00
Robin Dadswell
615facb3c4 fixed test due to GoodReads BookId change 2022-06-15 23:28:43 +01:00
171 changed files with 3699 additions and 1237 deletions

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an 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

View File

@@ -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:

View File

@@ -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
};

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View 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;

View File

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

View File

@@ -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;

View 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;

View File

@@ -33,7 +33,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.5",
"@microsoft/signalr": "6.0.7",
"@sentry/browser": "6.18.2",
"@sentry/integrations": "6.18.2",
"ansi-colors": "4.1.1",

View File

@@ -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="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>

View File

@@ -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>

View File

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

View File

@@ -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()
{

View File

@@ -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()
{

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Instrumentation;
@@ -60,6 +60,7 @@ 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 ***")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
@@ -70,6 +71,11 @@ 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""")]
// Webhooks - Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/readarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);

View File

@@ -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();

View File

@@ -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(@"(?<=[?& ;])[^=]*?(_?(?<!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,11 @@ 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)
};
private static readonly Regex CleanseRemoteIPRegex = new Regex(@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);

View File

@@ -127,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)
{

View File

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

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_author_for_trackfile()
{
var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<BookFile>(new SqlBuilder()).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)

View 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))");
}
}
}

View File

@@ -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\")");
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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);
}
}

View File

@@ -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>();
}
}
}
}

View File

@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenBooksForRefresh(List<Book> books)
{
Mocker.GetMock<IBookService>(MockBehavior.Strict)
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<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

View File

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

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.AuthorStats
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
{
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
private readonly IMainDatabase _database;
@@ -45,14 +45,14 @@ namespace NzbDrone.Core.AuthorStats
}
}
private SqlBuilder Builder() => new SqlBuilder()
.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)

View File

@@ -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)
{

View File

@@ -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()

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Data.Common;
using System.Data.SQLite;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
@@ -92,10 +94,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;
});

View File

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

View File

@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
public class LogDatabase : ILogDatabase
{
private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public LogDatabase(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();

View File

@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
public class MainDatabase : IMainDatabase
{
private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public MainDatabase(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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore
public class SqlBuilder
{
private readonly Dictionary<string, Clauses> _data = new Dictionary<string, Clauses>();
private readonly DatabaseType _databaseType;
public SqlBuilder(DatabaseType databaseType)
{
_databaseType = databaseType;
}
public int Sequence { get; private set; }
public DatabaseType DatabaseType => _databaseType;
public Template AddTemplate(string sql, dynamic parameters = null) =>
new Template(this, sql, parameters);

View File

@@ -50,17 +50,17 @@ namespace NzbDrone.Core.Datastore
public string SelectTemplate(Type x)
{
return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
}
public string DeleteTemplate(Type x)
{
return $"DELETE FROM {TableMap[x]} /**where**/";
return $"DELETE FROM \"{TableMap[x]}\" /**where**/";
}
public string PageCountTemplate(Type x)
{
return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
}
public bool IsValidSortKey(string sortKey)
@@ -91,6 +91,35 @@ namespace NzbDrone.Core.Datastore
return true;
}
public string GetSortKey(string sortKey)
{
string table = null;
if (sortKey.Contains('.'))
{
var split = sortKey.Split('.');
if (split.Length != 2)
{
return sortKey;
}
table = split[0];
sortKey = split[1];
}
if (table != null && !TableMap.Values.Contains(table, StringComparer.OrdinalIgnoreCase))
{
return sortKey;
}
if (!_allowedOrderBy.Contains(sortKey))
{
return sortKey;
}
return _allowedOrderBy.First(x => x.Equals(sortKey, StringComparison.OrdinalIgnoreCase));
}
}
public class LazyLoadedProperty
@@ -155,7 +184,7 @@ namespace NzbDrone.Core.Datastore
(db, parent) =>
{
var id = childIdSelector(parent);
return db.Query<TChild>(new SqlBuilder().Where<TChild>(x => x.Id == id)).SingleOrDefault();
return db.Query<TChild>(new SqlBuilder(db.DatabaseType).Where<TChild>(x => x.Id == id)).SingleOrDefault();
},
parent => childIdSelector(parent) > 0);
}

View File

@@ -108,15 +108,15 @@ namespace NzbDrone.Core.Datastore
.HasOne(a => a.Metadata, a => a.AuthorMetadataId)
.HasOne(a => a.QualityProfile, a => a.QualityProfileId)
.HasOne(s => s.MetadataProfile, s => s.MetadataProfileId)
.LazyLoad(a => a.Books, (db, a) => db.Query<Book>(new SqlBuilder().Where<Book>(b => b.AuthorMetadataId == a.AuthorMetadataId)).ToList(), a => a.AuthorMetadataId > 0);
.LazyLoad(a => a.Books, (db, a) => db.Query<Book>(new SqlBuilder(db.DatabaseType).Where<Book>(b => b.AuthorMetadataId == a.AuthorMetadataId)).ToList(), a => a.AuthorMetadataId > 0);
Mapper.Entity<Series>("Series").RegisterModel()
.Ignore(s => s.ForeignAuthorId)
.LazyLoad(s => s.LinkItems,
(db, series) => db.Query<SeriesBookLink>(new SqlBuilder().Where<SeriesBookLink>(s => s.SeriesId == series.Id)).ToList(),
(db, series) => db.Query<SeriesBookLink>(new SqlBuilder(db.DatabaseType).Where<SeriesBookLink>(s => s.SeriesId == series.Id)).ToList(),
s => s.Id > 0)
.LazyLoad(s => s.Books,
(db, series) => db.Query<Book>(new SqlBuilder()
(db, series) => db.Query<Book>(new SqlBuilder(db.DatabaseType)
.Join<Book, SeriesBookLink>((l, r) => l.Id == r.BookId)
.Join<SeriesBookLink, Series>((l, r) => l.SeriesId == r.Id)
.Where<Series>(s => s.Id == series.Id)).ToList(),
@@ -132,27 +132,27 @@ namespace NzbDrone.Core.Datastore
.Ignore(x => x.AuthorId)
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
.LazyLoad(x => x.BookFiles,
(db, book) => db.Query<BookFile>(new SqlBuilder()
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
.Join<BookFile, Edition>((l, r) => l.EditionId == r.Id)
.Where<Edition>(b => b.BookId == book.Id)).ToList(),
b => b.Id > 0)
.LazyLoad(x => x.Editions,
(db, book) => db.Query<Edition>(new SqlBuilder().Where<Edition>(e => e.BookId == book.Id)).ToList(),
(db, book) => db.Query<Edition>(new SqlBuilder(db.DatabaseType).Where<Edition>(e => e.BookId == book.Id)).ToList(),
b => b.Id > 0)
.LazyLoad(a => a.Author,
(db, book) => AuthorRepository.Query(db,
new SqlBuilder()
new SqlBuilder(db.DatabaseType)
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id)
.Where<Author>(a => a.AuthorMetadataId == book.AuthorMetadataId)).SingleOrDefault(),
a => a.AuthorMetadataId > 0)
.LazyLoad(b => b.SeriesLinks,
(db, book) => db.Query<SeriesBookLink>(new SqlBuilder().Where<SeriesBookLink>(s => s.BookId == book.Id)).ToList(),
(db, book) => db.Query<SeriesBookLink>(new SqlBuilder(db.DatabaseType).Where<SeriesBookLink>(s => s.BookId == book.Id)).ToList(),
b => b.Id > 0);
Mapper.Entity<Edition>("Editions").RegisterModel()
.HasOne(r => r.Book, r => r.BookId)
.LazyLoad(x => x.BookFiles,
(db, edition) => db.Query<BookFile>(new SqlBuilder().Where<BookFile>(f => f.EditionId == edition.Id)).ToList(),
(db, edition) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType).Where<BookFile>(f => f.EditionId == edition.Id)).ToList(),
b => b.Id > 0);
Mapper.Entity<BookFile>("BookFiles").RegisterModel()
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Datastore
.HasOne(f => f.Edition, f => f.EditionId)
.LazyLoad(x => x.Author,
(db, f) => AuthorRepository.Query(db,
new SqlBuilder()
new SqlBuilder(db.DatabaseType)
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id)
.Join<Author, Book>((l, r) => l.AuthorMetadataId == r.AuthorMetadataId)
.Join<Book, Edition>((l, r) => l.Id == r.BookId)

View File

@@ -1,390 +1,10 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilder : ExpressionVisitor
public abstract class WhereBuilder : ExpressionVisitor
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilder(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
public DynamicParameters Parameters { get; private set; }
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append("(");
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(")");
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string) || list.Type == typeof(List<string>)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append("(");
Visit(item);
_sb.Append(" IN ");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append("(");
_sb.Append(string.Join(", ", items));
_sb.Append(")");
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append(")");
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append("(");
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append("(");
Visit(body.Object);
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append("(");
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(")");
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
public DynamicParameters Parameters { get; protected set; }
}
}

View File

@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilderPostgres : WhereBuilder
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append('(');
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(')');
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append('(');
Visit(item);
_sb.Append(" = ANY (");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append("('{");
_sb.Append(string.Join(", ", items));
_sb.Append("}')");
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append("))");
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" ILIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" ILIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" ILIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(')');
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
}
}

View File

@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore
{
public class WhereBuilderSqlite : WhereBuilder
{
protected StringBuilder _sb;
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
{
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
_requireConcreteValue = requireConcreteValue;
_sb = new StringBuilder();
Parameters = new DynamicParameters();
if (filter != null)
{
Visit(filter);
}
}
private string AddParameter(object value, DbType? dbType = null)
{
_gotConcreteValue = true;
_paramCount++;
var name = _paramNamePrefix + "_P" + _paramCount;
Parameters.Add(name, value, dbType);
return '@' + name;
}
protected override Expression VisitBinary(BinaryExpression expression)
{
_sb.Append('(');
Visit(expression.Left);
_sb.AppendFormat(" {0} ", Decode(expression));
Visit(expression.Right);
_sb.Append(')');
return expression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
var method = expression.Method.Name;
switch (expression.Method.Name)
{
case "Contains":
ParseContainsExpression(expression);
break;
case "StartsWith":
ParseStartsWith(expression);
break;
case "EndsWith":
ParseEndsWith(expression);
break;
default:
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
throw new NotImplementedException(msg);
}
return expression;
}
protected override Expression VisitMemberAccess(MemberExpression expression)
{
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
var gotValue = TryGetRightValue(expression, out var value);
// Only use the SQL condition if the expression didn't resolve to an actual value
if (tableName != null && !gotValue)
{
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
}
else
{
if (value != null)
{
// string is IEnumerable<Char> but we don't want to pick up that case
var type = value.GetType();
var typeInfo = type.GetTypeInfo();
var isEnumerable =
type != typeof(string) && (
typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
(typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)));
var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
}
return expression;
}
protected override Expression VisitConstant(ConstantExpression expression)
{
if (expression.Value != null)
{
var paramName = AddParameter(expression.Value);
_sb.Append(paramName);
}
else
{
_gotConcreteValue = true;
_sb.Append("NULL");
}
return expression;
}
private bool TryGetConstantValue(Expression expression, out object result)
{
result = null;
if (expression is ConstantExpression constExp)
{
result = constExp.Value;
return true;
}
return false;
}
private bool TryGetPropertyValue(MemberExpression expression, out object result)
{
result = null;
if (expression.Expression is MemberExpression nested)
{
// Value is passed in as a property on a parent entity
var container = (nested.Expression as ConstantExpression)?.Value;
if (container == null)
{
return false;
}
var entity = GetFieldValue(container, nested.Member);
result = GetFieldValue(entity, expression.Member);
return true;
}
return false;
}
private bool TryGetVariableValue(MemberExpression expression, out object result)
{
result = null;
// Value is passed in as a variable
if (expression.Expression is ConstantExpression nested)
{
result = GetFieldValue(nested.Value, expression.Member);
return true;
}
return false;
}
private bool TryGetRightValue(Expression expression, out object value)
{
value = null;
if (TryGetConstantValue(expression, out value))
{
return true;
}
var memberExp = expression as MemberExpression;
if (TryGetPropertyValue(memberExp, out value))
{
return true;
}
if (TryGetVariableValue(memberExp, out value))
{
return true;
}
return false;
}
private object GetFieldValue(object entity, MemberInfo member)
{
if (member.MemberType == MemberTypes.Field)
{
return (member as FieldInfo).GetValue(entity);
}
if (member.MemberType == MemberTypes.Property)
{
return (member as PropertyInfo).GetValue(entity);
}
throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name));
}
private bool IsNullVariable(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant &&
TryGetConstantValue(expression, out var constResult) &&
constResult == null)
{
return true;
}
if (expression.NodeType == ExpressionType.MemberAccess &&
expression is MemberExpression member &&
((TryGetPropertyValue(member, out var result) && result == null) ||
(TryGetVariableValue(member, out result) && result == null)))
{
return true;
}
return false;
}
private string Decode(BinaryExpression expression)
{
if (IsNullVariable(expression.Right))
{
switch (expression.NodeType)
{
case ExpressionType.Equal: return "IS";
case ExpressionType.NotEqual: return "IS NOT";
}
}
switch (expression.NodeType)
{
case ExpressionType.AndAlso: return "AND";
case ExpressionType.And: return "AND";
case ExpressionType.Equal: return "=";
case ExpressionType.GreaterThan: return ">";
case ExpressionType.GreaterThanOrEqual: return ">=";
case ExpressionType.LessThan: return "<";
case ExpressionType.LessThanOrEqual: return "<=";
case ExpressionType.NotEqual: return "<>";
case ExpressionType.OrElse: return "OR";
case ExpressionType.Or: return "OR";
default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString()));
}
}
private void ParseContainsExpression(MethodCallExpression expression)
{
var list = expression.Object;
if (list != null && (list.Type == typeof(string)))
{
ParseStringContains(expression);
return;
}
ParseEnumerableContains(expression);
}
private void ParseEnumerableContains(MethodCallExpression body)
{
// Fish out the list and the item to compare
// It's in a different form for arrays and Lists
var list = body.Object;
Expression item;
if (list != null)
{
// Generic collection
item = body.Arguments[0];
}
else
{
// Static method
// Must be Enumerable.Contains(source, item)
if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2)
{
throw new NotSupportedException("Unexpected form of Enumerable.Contains");
}
list = body.Arguments[0];
item = body.Arguments[1];
}
_sb.Append('(');
Visit(item);
_sb.Append(" IN ");
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append('(');
_sb.Append(string.Join(", ", items));
_sb.Append(')');
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append(')');
}
private void ParseStringContains(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseStartsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE ");
Visit(body.Arguments[0]);
_sb.Append(" || '%')");
}
private void ParseEndsWith(MethodCallExpression body)
{
_sb.Append('(');
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
Visit(body.Arguments[0]);
_sb.Append(')');
}
public override string ToString()
{
var sql = _sb.ToString();
if (_requireConcreteValue && !_gotConcreteValue)
{
var e = new InvalidOperationException("WhereBuilder requires a concrete condition");
e.Data.Add("sql", sql);
throw e;
}
return sql;
}
}
}

View File

@@ -70,7 +70,7 @@ namespace NzbDrone.Core.HealthCheck
.ToDictionary(g => g.Key, g => g.ToArray());
}
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, IEvent message = null)
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, IEvent message = null, bool performServerChecks = false)
{
var results = new List<HealthCheck>();
@@ -86,7 +86,10 @@ namespace NzbDrone.Core.HealthCheck
}
}
results.AddRange(_serverSideNotificationService.GetServerChecks());
if (performServerChecks)
{
results.AddRange(_serverSideNotificationService.GetServerChecks());
}
foreach (var result in results)
{
@@ -112,17 +115,17 @@ namespace NzbDrone.Core.HealthCheck
{
if (message.Trigger == CommandTrigger.Manual)
{
PerformHealthCheck(_healthChecks);
PerformHealthCheck(_healthChecks, null, true);
}
else
{
PerformHealthCheck(_scheduledHealthChecks);
PerformHealthCheck(_scheduledHealthChecks, null, true);
}
}
public void HandleAsync(ApplicationStartedEvent message)
{
PerformHealthCheck(_startupHealthChecks);
PerformHealthCheck(_startupHealthChecks, null, true);
}
public void HandleAsync(IEvent message)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
@@ -23,24 +24,37 @@ namespace NzbDrone.Core.HealthCheck
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
private readonly Logger _logger;
public ServerSideNotificationService(IHttpClient client, IConfigFileProvider configFileProvider, IReadarrCloudRequestBuilder cloudRequestBuilder, Logger logger)
private readonly ICached<List<HealthCheck>> _cache;
public ServerSideNotificationService(IHttpClient client,
IConfigFileProvider configFileProvider,
IReadarrCloudRequestBuilder cloudRequestBuilder,
ICacheManager cacheManager,
Logger logger)
{
_client = client;
_configFileProvider = configFileProvider;
_cloudRequestBuilder = cloudRequestBuilder.Services;
_logger = logger;
_cache = cacheManager.GetCache<List<HealthCheck>>(GetType());
}
public List<HealthCheck> GetServerChecks()
{
return _cache.Get("ServerChecks", () => RetrieveServerChecks(), TimeSpan.FromHours(2));
}
private List<HealthCheck> RetrieveServerChecks()
{
var request = _cloudRequestBuilder.Create()
.Resource("/notification")
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
.AddQueryParam("branch", _configFileProvider.Branch)
.Build();
.Resource("/notification")
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
.AddQueryParam("branch", _configFileProvider.Branch)
.Build();
try
{
_logger.Trace("Getting server side health notifications");

View File

@@ -90,11 +90,11 @@ namespace NzbDrone.Core.History
public List<EntityHistory> FindDownloadHistory(int idAuthorId, QualityModel quality)
{
var allowed = new[] { EntityHistoryEventType.Grabbed, EntityHistoryEventType.DownloadFailed, EntityHistoryEventType.BookFileImported };
var allowed = new[] { (int)EntityHistoryEventType.Grabbed, (int)EntityHistoryEventType.DownloadFailed, (int)EntityHistoryEventType.BookFileImported };
return Query(h => h.AuthorId == idAuthorId &&
h.Quality == quality &&
allowed.Contains(h.EventType));
allowed.Contains((int)h.EventType));
}
public void DeleteForAuthor(int authorId)
@@ -102,7 +102,7 @@ namespace NzbDrone.Core.History
Delete(c => c.AuthorId == authorId);
}
protected override SqlBuilder PagedBuilder() => new SqlBuilder()
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id)
.Join<EntityHistory, Book>((h, a) => h.BookId == a.Id);

View File

@@ -1,4 +1,4 @@
using Dapper;
using Dapper;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
@@ -16,16 +16,32 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE RelativePath
LIKE '_:\%'
OR RelativePath
LIKE '\%'
OR RelativePath
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" = ANY (
SELECT ""Id"" FROM ""MetadataFiles""
WHERE ""RelativePath""
LIKE '_:\\%'
OR ""RelativePath""
LIKE '\\%'
OR ""RelativePath""
LIKE '/%'
)");
}
else
{
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""Id"" FROM ""MetadataFiles""
WHERE ""RelativePath""
LIKE '_:\%'
OR ""RelativePath""
LIKE '\%'
OR ""RelativePath""
LIKE '/%'
)");
}
}
}
}

View File

@@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM NamingConfig
WHERE ID NOT IN (
SELECT ID FROM NamingConfig
mapper.Execute(@"DELETE FROM ""NamingConfig""
WHERE ""Id"" NOT IN (
SELECT ""Id"" FROM ""NamingConfig""
LIMIT 1)");
}
}

View File

@@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM Users
WHERE ID NOT IN (
SELECT ID FROM Users
mapper.Execute(@"DELETE FROM ""Users""
WHERE ""Id"" NOT IN (
SELECT ""Id"" FROM ""Users""
LIMIT 1)");
}
}

View File

@@ -16,16 +16,29 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
using (var mapper = _database.OpenConnection())
var mapper = _database.OpenConnection();
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
mapper.Execute(@"DELETE FROM PendingReleases
WHERE Added < @TwoWeeksAgo
AND REASON IN @Reasons",
new
{
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
});
mapper.Execute(@"DELETE FROM ""PendingReleases""
WHERE ""Added"" < @TwoWeeksAgo
AND ""Reason"" = ANY (@Reasons)",
new
{
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
});
}
else
{
mapper.Execute(@"DELETE FROM ""PendingReleases""
WHERE ""Added"" < @TwoWeeksAgo
AND ""REASON"" IN @Reasons",
new
{
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
});
}
}
}

View File

@@ -23,12 +23,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 1
GROUP BY AuthorId, Consumer
HAVING COUNT(AuthorId) > 1
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE ""Type"" = 1
GROUP BY ""AuthorId"", ""Consumer""
HAVING COUNT(""AuthorId"") > 1
)");
}
}
@@ -37,12 +37,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type IN (2, 4)
GROUP BY BookId, Consumer
HAVING COUNT(BookId) > 1
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE ""Type"" IN (2, 4)
GROUP BY ""BookId"", ""Consumer""
HAVING COUNT(""BookId"") > 1
)");
}
}
@@ -51,12 +51,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type IN (2, 4)
GROUP BY BookFileId, Consumer
HAVING COUNT(BookFileId) > 1
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE ""Type"" IN (2, 4)
GROUP BY ""BookFileId"", ""Consumer""
HAVING COUNT(""BookFileId"") > 1
)");
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM AuthorMetadata
WHERE Id IN (
SELECT AuthorMetadata.Id FROM AuthorMetadata
LEFT OUTER JOIN Books ON Books.AuthorMetadataId = AuthorMetadata.Id
LEFT OUTER JOIN Authors ON Authors.AuthorMetadataId = AuthorMetadata.Id
WHERE Books.Id IS NULL AND Authors.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""AuthorMetadata""
WHERE ""Id"" IN (
SELECT ""AuthorMetadata"".""Id"" FROM ""AuthorMetadata""
LEFT OUTER JOIN ""Books"" ON ""Books"".""AuthorMetadataId"" = ""AuthorMetadata"".""Id""
LEFT OUTER JOIN ""Authors"" ON ""Authors"".""AuthorMetadataId"" = ""AuthorMetadata"".""Id""
WHERE ""Books"".""Id"" IS NULL AND ""Authors"".""Id"" IS NULL)");
}
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM Blocklist
WHERE Id IN (
SELECT Blocklist.Id FROM Blocklist
LEFT OUTER JOIN Authors
ON Blocklist.AuthorId = Authors.Id
WHERE Authors.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""Blocklist""
WHERE ""Id"" IN (
SELECT ""Blocklist"".""Id"" FROM ""Blocklist""
LEFT OUTER JOIN ""Authors""
ON ""Blocklist"".""AuthorId"" = ""Authors"".""Id""
WHERE ""Authors"".""Id"" IS NULL)");
}
}
}

View File

@@ -17,13 +17,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
using (var mapper = _database.OpenConnection())
{
// Unlink where track no longer exists
mapper.Execute(@"UPDATE BookFiles
SET EditionId = 0
WHERE Id IN (
SELECT BookFiles.Id FROM BookFiles
LEFT OUTER JOIN Editions
ON BookFiles.EditionId = Editions.Id
WHERE Editions.Id IS NULL)");
mapper.Execute(@"UPDATE ""BookFiles""
SET ""EditionId"" = 0
WHERE ""Id"" IN (
SELECT ""BookFiles"".""Id"" FROM ""BookFiles""
LEFT OUTER JOIN ""Editions""
ON ""BookFiles"".""EditionId"" = ""Editions"".""Id""
WHERE ""Editions"".""Id"" IS NULL)");
}
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM Books
WHERE Id IN (
SELECT Books.Id FROM Books
LEFT OUTER JOIN Authors
ON Books.AuthorMetadataId = Authors.AuthorMetadataId
WHERE Authors.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""Books""
WHERE ""Id"" IN (
SELECT ""Books"".""Id"" FROM ""Books""
LEFT OUTER JOIN ""Authors""
ON ""Books"".""AuthorMetadataId"" = ""Authors"".""AuthorMetadataId""
WHERE ""Authors"".""Id"" IS NULL)");
}
}
}

View File

@@ -14,15 +14,14 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM DownloadClientStatus
WHERE Id IN (
SELECT DownloadClientStatus.Id FROM DownloadClientStatus
LEFT OUTER JOIN DownloadClients
ON DownloadClientStatus.ProviderId = DownloadClients.Id
WHERE DownloadClients.Id IS NULL)");
}
var mapper = _database.OpenConnection();
mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
WHERE ""Id"" IN (
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
LEFT OUTER JOIN ""DownloadClients""
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
WHERE ""DownloadClients"".""Id"" IS NULL)");
}
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM Editions
WHERE Id IN (
SELECT Editions.Id FROM Editions
LEFT OUTER JOIN Books
ON Editions.BookId = Books.Id
WHERE Books.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""Editions""
WHERE ""Id"" IN (
SELECT ""Editions"".""Id"" FROM ""Editions""
LEFT OUTER JOIN ""Books""
ON ""Editions"".""BookId"" = ""Books"".""Id""
WHERE ""Books"".""Id"" IS NULL)");
}
}
}

View File

@@ -1,4 +1,4 @@
using Dapper;
using Dapper;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
@@ -22,12 +22,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM History
WHERE Id IN (
SELECT History.Id FROM History
LEFT OUTER JOIN Authors
ON History.AuthorId = Authors.Id
WHERE Authors.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""History""
WHERE ""Id"" IN (
SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN ""Authors""
ON ""History"".""AuthorId"" = ""Authors"".""Id""
WHERE ""Authors"".""Id"" IS NULL)");
}
}
@@ -35,12 +35,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM History
WHERE Id IN (
SELECT History.Id FROM History
LEFT OUTER JOIN Books
ON History.BookId = Books.Id
WHERE Books.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""History""
WHERE ""Id"" IN (
SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN ""Books""
ON ""History"".""BookId"" = ""Books"".""Id""
WHERE ""Books"".""Id"" IS NULL)");
}
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM ImportListStatus
WHERE Id IN (
SELECT ImportListStatus.Id FROM ImportListStatus
LEFT OUTER JOIN ImportLists
ON ImportListStatus.ProviderId = ImportLists.Id
WHERE ImportLists.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""ImportListStatus""
WHERE ""Id"" IN (
SELECT ""ImportListStatus"".""Id"" FROM ""ImportListStatus""
LEFT OUTER JOIN ""ImportLists""
ON ""ImportListStatus"".""ProviderId"" = ""ImportLists"".""Id""
WHERE ""ImportLists"".""Id"" IS NULL)");
}
}
}

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM IndexerStatus
WHERE Id IN (
SELECT IndexerStatus.Id FROM IndexerStatus
LEFT OUTER JOIN Indexers
ON IndexerStatus.ProviderId = Indexers.Id
WHERE Indexers.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""IndexerStatus""
WHERE ""Id"" IN (
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
LEFT OUTER JOIN ""Indexers""
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
WHERE ""Indexers"".""Id"" IS NULL)");
}
}
}

View File

@@ -25,12 +25,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN Authors
ON MetadataFiles.AuthorId = Authors.Id
WHERE Authors.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN ""Authors""
ON ""MetadataFiles"".""AuthorId"" = ""Authors"".""Id""
WHERE ""Authors"".""Id"" IS NULL)");
}
}
@@ -38,13 +38,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN Books
ON MetadataFiles.BookId = Books.Id
WHERE MetadataFiles.BookId > 0
AND Books.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN ""Books""
ON ""MetadataFiles"".""BookId"" = ""Books"".""Id""
WHERE ""MetadataFiles"".""BookId"" > 0
AND ""Books"".""Id"" IS NULL)");
}
}
@@ -52,13 +52,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN BookFiles
ON MetadataFiles.BookFileId = BookFiles.Id
WHERE MetadataFiles.BookFileId > 0
AND BookFiles.Id IS NULL)");
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN ""BookFiles""
ON ""MetadataFiles"".""BookFileId"" = ""BookFiles"".""Id""
WHERE ""MetadataFiles"".""BookFileId"" > 0
AND ""BookFiles"".""Id"" IS NULL)");
}
}
@@ -66,11 +66,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type IN (2, 4)
AND BookId = 0)");
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""Id"" FROM ""MetadataFiles""
WHERE ""Type"" IN (2, 4)
AND ""BookId"" = 0)");
}
}
@@ -78,11 +78,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type IN (2, 4)
AND BookFileId = 0)");
mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE ""Id"" IN (
SELECT ""Id"" FROM ""MetadataFiles""
WHERE ""Type"" IN (2, 4)
AND ""BookFileId"" = 0)");
}
}
}

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