mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-13 15:54:10 -04:00
Compare commits
104 Commits
v0.1.5.116
...
http2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa304dcaca | ||
|
|
e04133d34a | ||
|
|
07575ae239 | ||
|
|
8e7acd8946 | ||
|
|
3ecc926298 | ||
|
|
1e532624af | ||
|
|
8a5194e604 | ||
|
|
8a73cf72c2 | ||
|
|
76982c5988 | ||
|
|
b9dfe5e359 | ||
|
|
a5e13ca776 | ||
|
|
e2ddfbff9c | ||
|
|
66b4c7891d | ||
|
|
480a76c290 | ||
|
|
1373ab255d | ||
|
|
1dc00eb445 | ||
|
|
a366bec684 | ||
|
|
ecca6e9f49 | ||
|
|
03db7a9bbd | ||
|
|
9cb04466c1 | ||
|
|
2bae37d0c5 | ||
|
|
0dbd23c52b | ||
|
|
66a6311dcc | ||
|
|
c5b111530c | ||
|
|
77724a50a4 | ||
|
|
22cbd01c57 | ||
|
|
fd55a624a7 | ||
|
|
75984e954e | ||
|
|
3fce120578 | ||
|
|
6e8fb22c71 | ||
|
|
8ec7a4898d | ||
|
|
642848d331 | ||
|
|
c9e6a0339e | ||
|
|
25620e8670 | ||
|
|
5b804e8f3a | ||
|
|
548db6a5cd | ||
|
|
7f28f64cbe | ||
|
|
9bad31af84 | ||
|
|
01c7a05841 | ||
|
|
9859b4a3d9 | ||
|
|
177084fe8b | ||
|
|
c57a91bc64 | ||
|
|
ca67a40c72 | ||
|
|
de7505bbe6 | ||
|
|
97956ce951 | ||
|
|
8a38e124fd | ||
|
|
38fcffe871 | ||
|
|
4c7b5a47d3 | ||
|
|
34597e6ecb | ||
|
|
735be4f467 | ||
|
|
1c737d77fb | ||
|
|
55788ac04d | ||
|
|
d108ab0339 | ||
|
|
5928eea83e | ||
|
|
27898aa3b5 | ||
|
|
5e3322c538 | ||
|
|
80c31e8660 | ||
|
|
46401ee187 | ||
|
|
3610becc64 | ||
|
|
06d9c157d8 | ||
|
|
d0d1f40128 | ||
|
|
383d5464b7 | ||
|
|
62d15536df | ||
|
|
147cdf2cce | ||
|
|
dd27d69e97 | ||
|
|
32fd0911a2 | ||
|
|
0e6ec58a83 | ||
|
|
69f5963f6f | ||
|
|
6ca708f523 | ||
|
|
9e7af8369e | ||
|
|
b05d8c930d | ||
|
|
6b886b938c | ||
|
|
4a7bf39723 | ||
|
|
7fcd320e23 | ||
|
|
88677ce236 | ||
|
|
d2cf060473 | ||
|
|
3b7b72d4e1 | ||
|
|
4e69b80a98 | ||
|
|
0f52258d53 | ||
|
|
4eadd4cb2f | ||
|
|
579b8a3d3b | ||
|
|
849b3de7d3 | ||
|
|
8855b2846d | ||
|
|
c64addb976 | ||
|
|
fab1304bcd | ||
|
|
bd834fb4d7 | ||
|
|
dcee9582bd | ||
|
|
89e500edfd | ||
|
|
ea83020714 | ||
|
|
6d62744667 | ||
|
|
08c68e26c1 | ||
|
|
574568e71d | ||
|
|
c83c818380 | ||
|
|
a2df38b1ca | ||
|
|
89510c4a65 | ||
|
|
b5a2f68bde | ||
|
|
1ffab661da | ||
|
|
bf0a627a4e | ||
|
|
df764ce8b4 | ||
|
|
a61d4ab88c | ||
|
|
01e7e924c4 | ||
|
|
5f5df99dab | ||
|
|
77e40e8e53 | ||
|
|
d3853c1a54 |
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
'Type: Support':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
close: true
|
||||
|
||||
'Type: Indexer Request':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
|
||||
close: true
|
||||
23
.github/workflows/label-actions.yml
vendored
Normal file
23
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
discussion:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v2
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
21
.github/workflows/support.yml
vendored
21
.github/workflows/support.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/prowlarr)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -188,6 +188,10 @@ packages.config.md5sum
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
64
README.md
64
README.md
@@ -4,74 +4,84 @@
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
Prowlarr is an indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
|
||||
## Major Features Include:
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
|
||||
## Major Features Include
|
||||
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP
|
||||
- Usenet support for any Newznab compatible indexer via "Generic Newznab"
|
||||
- Torrent support for over 500 trackers with more added all the time
|
||||
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
|
||||
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr/Mylar3, so no manual configuration of the other applications are required
|
||||
- Support for custom YML definitions via Cardigann that includes JSON and XML parsing
|
||||
- Indexer Sync to Lidarr/Mylar3/Radarr/Readarr/Sonarr, so no manual configuration of the other applications are required
|
||||
- Indexer history and statistics
|
||||
- Manual searching of Trackers & Indexers at a category level
|
||||
- Support for pushing releases directly to your download clients from Prowlarr
|
||||
- Parameter based manual searching
|
||||
- Support for pushing multiple releases at once directly to your download clients from Prowlarr
|
||||
- Indexer health and status notifications
|
||||
- Per Indexer proxy support (SOCKS4, SOCKS5, HTTP, Flaresolverr)
|
||||
|
||||
## Support
|
||||
|
||||
Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://github.com/Prowlarr/Prowlarr/issues)
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
## Indexers/Trackers
|
||||
## Indexers & Trackers
|
||||
|
||||
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
|
||||
[](https://wiki.servarr.com/en/prowlarr/supported-indexers)
|
||||
|
||||
[Indexer Requests](https://requests.prowlarr.com)
|
||||
- Request or vote on an existing request for a new tracker/indexer
|
||||
[](https://requests.prowlarr.com)
|
||||
|
||||
## Contributors & Developers
|
||||
|
||||
[API Documentation](https://prowlarr.com/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/prowlarr/contributing)
|
||||
- [YML Indexer Defintion (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
|
||||
- [YML Indexer Definition (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
|
||||
[](https://github.com/Prowlarr/Prowlarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Prowlarr#backer)
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/backers.svg?width=890"></a>
|
||||

|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Prowlarr#sponsor)
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/sponsors.svg?width=890"></a>
|
||||

|
||||
|
||||
## Mega Sponsors
|
||||
|
||||
<img src="https://opencollective.com/Prowlarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||

|
||||
|
||||
## JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
|
||||
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
- [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
- [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
- [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2021
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2022
|
||||
|
||||
Icon Credit:
|
||||
<a href="https://www.freepik.com/vectors/box">Box vector created by freepik - www.freepik.com</a>
|
||||
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)
|
||||
|
||||
@@ -7,7 +7,7 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.5'
|
||||
majorVersion: '0.1.10'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -29,6 +29,7 @@ pr:
|
||||
paths:
|
||||
exclude:
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Prowlarr.API.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -163,7 +164,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -816,7 +816,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
@@ -825,6 +824,59 @@ stages:
|
||||
FORCE_COLOR: 0
|
||||
YARN_CACHE_FOLDER: $(yarnCacheFolder)
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
condition: |
|
||||
and
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
)
|
||||
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
fetchDepth: 1
|
||||
- bash: ./docs.sh Windows
|
||||
displayName: Create openapi.json
|
||||
- bash: |
|
||||
git config --global user.email "development@lidarr.audio"
|
||||
git config --global user.name "Servarr"
|
||||
git checkout -b api-docs
|
||||
git add .
|
||||
if git status | grep -q modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
echo "No changes since last run"
|
||||
fi
|
||||
displayName: Commit API Doc Change
|
||||
continueOnError: true
|
||||
env:
|
||||
GITHUBTOKEN: $(githubToken)
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*openapi.json
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/api_docs
|
||||
artifact: 'APIDocs'
|
||||
displayName: Publish API Docs Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
dependsOn: Prepare
|
||||
|
||||
38
docs.sh
Normal file
38
docs.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
WHERE="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
WHERE="osx-x64"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
slnFile=src/Prowlarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.2.3 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
||||
|
||||
sleep 10
|
||||
|
||||
kill %1
|
||||
|
||||
exit 0
|
||||
@@ -77,7 +77,9 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>Maintenance release</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -166,7 +166,9 @@ class FilterBuilderModalContent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>Filters</div>
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
|
||||
@@ -68,6 +68,7 @@ function ProviderFieldFormGroup(props) {
|
||||
label,
|
||||
helpText,
|
||||
helpLink,
|
||||
placeholder,
|
||||
value,
|
||||
type,
|
||||
advanced,
|
||||
@@ -100,6 +101,7 @@ function ProviderFieldFormGroup(props) {
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpLink={helpLink}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
values={getSelectValues(selectOptions)}
|
||||
errors={errors}
|
||||
@@ -125,6 +127,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||
label: PropTypes.string,
|
||||
helpText: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
type: PropTypes.string.isRequired,
|
||||
advanced: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -41,7 +41,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Query Results'}
|
||||
title={translate('QueryResults')}
|
||||
data={queryResults ? queryResults : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Categories'}
|
||||
title={translate('Categories')}
|
||||
data={categories ? categories : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -57,7 +57,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Source'}
|
||||
title={translate('Source')}
|
||||
data={source}
|
||||
/>
|
||||
}
|
||||
@@ -65,7 +65,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Url'}
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -93,7 +93,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Source'}
|
||||
title={translate('Source')}
|
||||
data={source ? source : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Title'}
|
||||
title={translate('Title')}
|
||||
data={title ? title : '-'}
|
||||
/>
|
||||
}
|
||||
@@ -109,7 +109,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!data &&
|
||||
<DescriptionListItem
|
||||
title={'Url'}
|
||||
title={translate('Url')}
|
||||
data={url ? <Link to={url}>{translate('Link')}</Link> : '-'}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class HistoryOptions extends Component {
|
||||
|
||||
@@ -56,14 +57,14 @@ class HistoryOptions extends Component {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>History Cleanup</FormLabel>
|
||||
<FormLabel>{translate('HistoryCleanup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="historyCleanupDays"
|
||||
value={historyCleanupDays}
|
||||
helpText="Set to 0 to disable automatic cleanup"
|
||||
helpTextWarning="History items older than the selected number of days will be cleaned up automatically"
|
||||
helpText={translate('HistoryCleanupDaysHelpText')}
|
||||
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}
|
||||
onChange={this.onGlobalInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -56,6 +56,21 @@ const protocols = [
|
||||
}
|
||||
];
|
||||
|
||||
const privacyLevels = [
|
||||
{
|
||||
key: 'private',
|
||||
value: translate('Private')
|
||||
},
|
||||
{
|
||||
key: 'semiPrivate',
|
||||
value: translate('SemiPrivate')
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
value: translate('Public')
|
||||
}
|
||||
];
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
@@ -99,10 +114,6 @@ class AddIndexerModalContent extends Component {
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((language) => ({ key: language, value: language }));
|
||||
|
||||
const privacyLevels = Array.from(new Set(indexers.map(({ privacy }) => privacy)))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((privacy) => ({ key: privacy, value: privacy }));
|
||||
|
||||
const filteredIndexers = indexers.filter((indexer) => {
|
||||
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import React, { Component } from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
class SelectIndexerRow extends Component {
|
||||
@@ -47,7 +49,7 @@ class SelectIndexerRow extends Component {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{privacy}
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
|
||||
@@ -159,7 +159,7 @@ function EditIndexerModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText="Use tags to specify default clients, specify Indexer Proxies, or just to organize your indexers."
|
||||
helpText={translate('IndexerTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -302,14 +302,14 @@ class IndexerIndex extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={'Add Indexer'}
|
||||
label={translate('AddIndexer')}
|
||||
iconName={icons.ADD}
|
||||
spinningName={icons.ADD}
|
||||
onPress={this.onAddIndexerPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={'Test All Indexers'}
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
isDisabled={hasNoIndexer}
|
||||
@@ -321,13 +321,13 @@ class IndexerIndex extends Component {
|
||||
{
|
||||
isMovieEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={'Indexers'}
|
||||
label={translate('Indexers')}
|
||||
iconName={icons.MOVIE_CONTINUING}
|
||||
isDisabled={hasNoIndexer}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={'Mass Editor'}
|
||||
label={translate('MassEditor')}
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoIndexer}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
|
||||
@@ -240,14 +240,14 @@ class IndexerIndexRow extends Component {
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
title={'Indexer info'}
|
||||
title={translate('IndexerInfo')}
|
||||
onPress={this.onIndexerInfoPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={'Website'}
|
||||
title={translate('Website')}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/>
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import StatsFilterMenu from './StatsFilterMenu';
|
||||
import styles from './Stats.css';
|
||||
|
||||
function getAverageResponseTimeData(indexerStats) {
|
||||
@@ -144,14 +146,29 @@ function Stats(props) {
|
||||
item,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
error,
|
||||
filters,
|
||||
selectedFilterKey,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
const isLoaded = !!(!error && isPopulated);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar />
|
||||
<PageToolbar>
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
<StatsFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
@@ -232,6 +249,10 @@ Stats.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerStats } from 'Store/Actions/indexerStatsActions';
|
||||
import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
import Stats from './Stats';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -12,9 +12,16 @@ function createMapStateToProps() {
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexerStats
|
||||
};
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setIndexerStatsFilter({ selectedFilterKey }));
|
||||
},
|
||||
dispatchFetchIndexerStats() {
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class StatsConnector extends Component {
|
||||
|
||||
@@ -22,7 +29,7 @@ class StatsConnector extends Component {
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchIndexers();
|
||||
this.props.dispatchFetchIndexerStats();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -38,7 +45,7 @@ class StatsConnector extends Component {
|
||||
}
|
||||
|
||||
StatsConnector.propTypes = {
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
dispatchFetchIndexerStats: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(StatsConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);
|
||||
|
||||
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function StatsFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
isDisabled,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={isDisabled}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StatsFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
StatsFilterMenu.defaultProps = {
|
||||
showCustomFilters: false
|
||||
};
|
||||
|
||||
export default StatsFilterMenu;
|
||||
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.indexerStats.items,
|
||||
(state) => state.indexerStats.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'indexerStats'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setIndexerStatsFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
@@ -299,7 +299,7 @@ class SearchIndexRow extends Component {
|
||||
<IconButton
|
||||
className={styles.downloadLink}
|
||||
name={icons.SAVE}
|
||||
title={'Save'}
|
||||
title={translate('Save')}
|
||||
to={downloadUrl}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -61,7 +61,7 @@ class Applications extends Component {
|
||||
return (
|
||||
<FieldSet legend={translate('Applications')}>
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load application list"
|
||||
errorMessage={translate('UnableToLoadApplicationList')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.applications}>
|
||||
|
||||
@@ -61,7 +61,7 @@ class IndexerProxies extends Component {
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<FieldSet legend={translate('IndexerProxies')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadIndexerProxies')}
|
||||
{...otherProps}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Notification extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteNotificationModalClose= () => {
|
||||
onDeleteNotificationModalClose = () => {
|
||||
this.setState({ isDeleteNotificationModalOpen: false });
|
||||
};
|
||||
|
||||
@@ -61,12 +61,14 @@ class Notification extends Component {
|
||||
onRename,
|
||||
onDelete,
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnGrab,
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnDelete,
|
||||
supportsOnHealthIssue
|
||||
supportsOnHealthIssue,
|
||||
supportsOnApplicationUpdate
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -82,53 +84,62 @@ class Notification extends Component {
|
||||
{
|
||||
supportsOnGrab && onGrab &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Grab
|
||||
{translate('OnGrab')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDelete && onDelete &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Delete
|
||||
{translate('OnDelete')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDownload && onDownload &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Import
|
||||
{translate('OnImport')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnUpgrade && onDownload && onUpgrade &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Upgrade
|
||||
{translate('OnUpgrade')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnRename && onRename &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Rename
|
||||
{translate('OnRename')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Health Issue
|
||||
{translate('OnHealthIssue')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete &&
|
||||
supportsOnApplicationUpdate && onApplicationUpdate ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('OnApplicationUpdate')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete && !onApplicationUpdate ?
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label>
|
||||
{translate('Disabled')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<EditNotificationModalConnector
|
||||
@@ -161,12 +172,14 @@ Notification.propTypes = {
|
||||
onRename: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.bool.isRequired,
|
||||
onHealthIssue: PropTypes.bool.isRequired,
|
||||
onApplicationUpdate: PropTypes.bool.isRequired,
|
||||
supportsOnGrab: PropTypes.bool.isRequired,
|
||||
supportsOnDownload: PropTypes.bool.isRequired,
|
||||
supportsOnDelete: PropTypes.bool.isRequired,
|
||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnRename: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteNotification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ function NotificationEventItems(props) {
|
||||
|
||||
const {
|
||||
onHealthIssue,
|
||||
onApplicationUpdate,
|
||||
supportsOnHealthIssue,
|
||||
includeHealthWarnings
|
||||
includeHealthWarnings,
|
||||
supportsOnApplicationUpdate
|
||||
} = item;
|
||||
|
||||
return (
|
||||
@@ -53,6 +55,17 @@ function NotificationEventItems(props) {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onApplicationUpdate"
|
||||
helpText={translate('OnApplicationUpdateHelpText')}
|
||||
isDisabled={!supportsOnApplicationUpdate.value}
|
||||
{...onApplicationUpdate}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
@@ -67,7 +67,7 @@ function TagDetailsModalContent(props) {
|
||||
|
||||
{
|
||||
!!indexerProxies.length &&
|
||||
<FieldSet legend={translate('Indexer Proxies')}>
|
||||
<FieldSet legend={translate('IndexerProxies')}>
|
||||
{
|
||||
indexerProxies.map((item) => {
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,6 @@ function createRemoveItemHandler(section, url) {
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/${id}?${$.param(queryParams, true)}`,
|
||||
dataType: 'text',
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ export default {
|
||||
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
|
||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
@@ -15,30 +20,140 @@ export const defaultState = {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: {},
|
||||
start: null,
|
||||
end: null,
|
||||
|
||||
details: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: []
|
||||
}
|
||||
},
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastSeven',
|
||||
label: 'Last 7 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastThirty',
|
||||
label: 'Last 30 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastNinety',
|
||||
label: 'Last 90 Days',
|
||||
filters: []
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'startDate',
|
||||
label: 'Start Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
}
|
||||
],
|
||||
selectedFilterKey: 'all'
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'indexerStats.customFilters',
|
||||
'indexerStats.selectedFilterKey'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
|
||||
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerStats = createThunk(FETCH_INDEXER_STATS);
|
||||
export const setIndexerStatsFilter = createThunk(SET_INDEXER_STATS_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_INDEXER_STATS]: createFetchHandler(section, '/indexerStats')
|
||||
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const indexerStats = state.indexerStats;
|
||||
|
||||
const requestParams = {
|
||||
endDate: moment().toISOString()
|
||||
};
|
||||
|
||||
if (indexerStats.selectedFilterKey !== 'all') {
|
||||
let dayCount = 7;
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastThirty') {
|
||||
dayCount = 30;
|
||||
}
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastNinety') {
|
||||
dayCount = 90;
|
||||
}
|
||||
|
||||
requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString();
|
||||
}
|
||||
|
||||
const basesAttrs = {
|
||||
section,
|
||||
isFetching: true
|
||||
};
|
||||
|
||||
const attrs = basesAttrs;
|
||||
|
||||
dispatch(set(attrs));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/indexerStats',
|
||||
data: requestParams
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[SET_INDEXER_STATS_FILTER]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, ...payload }));
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -20,10 +20,11 @@ class About extends Component {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isMono,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -48,14 +49,6 @@ class About extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<DescriptionListItem
|
||||
title={translate('MonoVersion')}
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
@@ -77,6 +70,11 @@ class About extends Component {
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -114,9 +112,10 @@ About.propTypes = {
|
||||
packageVersion: PropTypes.string,
|
||||
packageAuthor: PropTypes.string,
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
|
||||
@@ -199,7 +199,7 @@ class QueuedTaskRow extends Component {
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title="User-Agent provided by the app that called the API">
|
||||
<span className={styles.userAgent} title={translate('UserAgentProvidedByTheAppThatCalledTheAPI')}>
|
||||
from: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#464b51" />
|
||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#e66001" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||
|
||||
|
||||
@@ -229,11 +229,6 @@ namespace NzbDrone.Common.Test.Http
|
||||
[Test]
|
||||
public void should_follow_redirects_to_https()
|
||||
{
|
||||
if (typeof(TDispatcher) == typeof(ManagedHttpDispatcher) && PlatformInfo.IsMono)
|
||||
{
|
||||
Assert.Ignore("Will fail on tls1.2 via managed dispatcher, ignore.");
|
||||
}
|
||||
|
||||
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
|
||||
.AddQueryParam("url", $"https://radarr.video/")
|
||||
.Build();
|
||||
|
||||
@@ -26,17 +26,21 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
//Indexer Responses
|
||||
|
||||
// avistaz response
|
||||
// avistaz response
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// danish bytes response
|
||||
// danish bytes response
|
||||
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// nzbgeek & usenet response
|
||||
// nzbgeek & usenet response
|
||||
[TestCase(@"<guid isPermaLink=""true"">https://api.nzbgeek.info/api?t=details&id=2b51db35e1910123321025a12b9933d2&apikey=2b51db35e1910123321025a12b9933d2</guid>")]
|
||||
|
||||
// UNIT3D Response
|
||||
[TestCase(@"""download_link"":""https://blutopia.xyz/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@"""download_link"":""https://desitorrents.tv/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
private readonly Logger _logger;
|
||||
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||
|
||||
public RuntimeInfo(IHostLifetime hostLifetime, Logger logger)
|
||||
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
|
||||
@@ -258,6 +258,11 @@ namespace NzbDrone.Common.Extensions
|
||||
return appFolderInfo.AppDataFolder;
|
||||
}
|
||||
|
||||
public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "asp");
|
||||
}
|
||||
|
||||
public static string GetLogFolder(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "logs");
|
||||
|
||||
@@ -34,19 +34,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
||||
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.None;
|
||||
webRequest.Headers.Add("Accept-Encoding", "gzip");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Deflate is not a standard and could break depending on implementation.
|
||||
// we should just stick with the more compatible Gzip
|
||||
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
}
|
||||
// Deflate is not a standard and could break depending on implementation.
|
||||
// we should just stick with the more compatible Gzip
|
||||
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
|
||||
webRequest.Method = request.Method.ToString();
|
||||
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||
@@ -127,19 +118,6 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
try
|
||||
{
|
||||
data = await responseStream.ToBytes();
|
||||
|
||||
if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
|
||||
{
|
||||
using (var compressedStream = new MemoryStream(data))
|
||||
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||||
using (var decompressedStream = new MemoryStream())
|
||||
{
|
||||
gzip.CopyTo(decompressedStream);
|
||||
data = decompressedStream.ToArray();
|
||||
}
|
||||
|
||||
httpWebResponse.Headers.Remove("Content-Encoding");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -254,7 +232,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
webRequest.TransferEncoding = header.Value;
|
||||
break;
|
||||
case "User-Agent":
|
||||
throw new NotSupportedException("User-Agent other than Prowlarr not allowed.");
|
||||
webRequest.UserAgent = header.Value;
|
||||
break;
|
||||
case "Proxy-Connection":
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
|
||||
@@ -91,7 +91,9 @@ namespace NzbDrone.Common.Http
|
||||
request.ContentData = null;
|
||||
}
|
||||
|
||||
response = await ExecuteRequestAsync(request, cookieContainer);
|
||||
var redirectContainer = HandleRedirectCookies(request, response);
|
||||
|
||||
response = await ExecuteRequestAsync(request, redirectContainer);
|
||||
}
|
||||
while (response.HasHttpRedirect);
|
||||
}
|
||||
@@ -162,6 +164,41 @@ namespace NzbDrone.Common.Http
|
||||
return response;
|
||||
}
|
||||
|
||||
private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
|
||||
{
|
||||
var sourceContainer = new CookieContainer();
|
||||
|
||||
var responseCookies = response.GetCookies();
|
||||
|
||||
if (responseCookies.Count != 0)
|
||||
{
|
||||
foreach (var pair in responseCookies)
|
||||
{
|
||||
Cookie cookie;
|
||||
if (pair.Value == null)
|
||||
{
|
||||
cookie = new Cookie(pair.Key, "", "/")
|
||||
{
|
||||
Expires = DateTime.Now.AddDays(-1)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
cookie = new Cookie(pair.Key, pair.Value, "/")
|
||||
{
|
||||
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
|
||||
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
|
||||
Expires = DateTime.Now.AddHours(1)
|
||||
};
|
||||
}
|
||||
|
||||
sourceContainer.Add((Uri)request.Url, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceContainer;
|
||||
}
|
||||
|
||||
private CookieContainer InitializeRequestCookies(HttpRequest request)
|
||||
{
|
||||
lock (_cookieContainerCache)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -107,6 +107,18 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
|
||||
public string UserAgent
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetSingleValue("User-Agent");
|
||||
}
|
||||
set
|
||||
{
|
||||
SetSingleValue("User-Agent", value);
|
||||
}
|
||||
}
|
||||
|
||||
public string Accept
|
||||
{
|
||||
get
|
||||
|
||||
@@ -11,21 +11,19 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
Request = request;
|
||||
Headers = headers;
|
||||
Cookies = cookies;
|
||||
ResponseData = binaryData;
|
||||
StatusCode = statusCode;
|
||||
ElapsedTime = elapsedTime;
|
||||
}
|
||||
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
public HttpResponse(HttpRequest request, HttpHeader headers, string content, long elapsedTime = 0, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
Request = request;
|
||||
Headers = headers;
|
||||
Cookies = cookies;
|
||||
ResponseData = Headers.GetEncodingFromContentType().GetBytes(content);
|
||||
_content = content;
|
||||
StatusCode = statusCode;
|
||||
@@ -34,7 +32,6 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public HttpRequest Request { get; private set; }
|
||||
public HttpHeader Headers { get; private set; }
|
||||
public CookieCollection Cookies { get; private set; }
|
||||
public HttpStatusCode StatusCode { get; private set; }
|
||||
public long ElapsedTime { get; private set; }
|
||||
public byte[] ResponseData { get; private set; }
|
||||
@@ -92,9 +89,14 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
foreach (Cookie cookie in Cookies)
|
||||
var setCookieHeaders = CookieUtil.CookieHeaderToDictionary();
|
||||
foreach (var cookie in setCookieHeaders)
|
||||
{
|
||||
result[cookie.Name] = cookie.Value;
|
||||
var match = RegexSetCookie.Match(cookie);
|
||||
if (match.Success)
|
||||
{
|
||||
result[match.Groups[1].Value] = match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -21,6 +21,9 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// UNIT3D
|
||||
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Path
|
||||
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -38,16 +38,6 @@ namespace NzbDrone.Common.Instrumentation
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
if ((exception is TypeInitializationException && exception.InnerException is DllNotFoundException) ||
|
||||
exception is DllNotFoundException)
|
||||
{
|
||||
Logger.Debug(exception, "Minor Fail: " + exception.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("EPIC FAIL: {0}", exception);
|
||||
Logger.Fatal(exception, "EPIC FAIL.");
|
||||
}
|
||||
|
||||
@@ -106,13 +106,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Debug = false;
|
||||
o.DiagnosticLevel = SentryLevel.Debug;
|
||||
o.Release = BuildInfo.Release;
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// Mono 6.0 broke GzipStream.WriteAsync
|
||||
// TODO: Check specific version
|
||||
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
|
||||
}
|
||||
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
@@ -155,7 +148,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
{
|
||||
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
|
||||
|
||||
if (osInfo.Name != null && PlatformInfo.IsMono)
|
||||
if (osInfo.Name != null && !OsInfo.IsWindows)
|
||||
{
|
||||
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
|
||||
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
|
||||
|
||||
@@ -366,11 +366,6 @@ namespace NzbDrone.Common.Processes
|
||||
|
||||
private (string Path, string Args) GetPathAndArgs(string path, string args)
|
||||
{
|
||||
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("mono", $"--debug {path} {args}");
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return ("cmd.exe", $"/c {path} {args}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
@@ -38,6 +38,7 @@ namespace NzbDrone.Core.Test.Datastore.SqliteSchemaDumperTests
|
||||
result.Name.Should().Be(tableName);
|
||||
result.Columns.Count.Should().Be(1);
|
||||
result.Columns.First().Name.Should().Be(columnName);
|
||||
result.Columns.First().IsIdentity.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase(@"CREATE INDEX TestIndex ON TestTable (MyId)", "TestIndex", "TestTable", "MyId")]
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Rarbg;
|
||||
@@ -51,7 +52,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
|
||||
torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=Prowlarr");
|
||||
torrentInfo.InfoUrl.Should().Be($"https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id={BuildInfo.AppName}");
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
|
||||
torrentInfo.Size.Should().Be(564198371);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using FluentValidation.Results;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Notifications;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.NotificationTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class NotificationBaseFixture : TestBase
|
||||
{
|
||||
private class TestSetting : IProviderConfig
|
||||
{
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestNotificationWithApplicationUpdate : NotificationBase<TestSetting>
|
||||
{
|
||||
public override string Name => "TestNotification";
|
||||
public override string Link => "";
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
TestLogger.Info("OnApplicationUpdate was called");
|
||||
}
|
||||
}
|
||||
|
||||
private class TestNotificationWithAllEvents : NotificationBase<TestSetting>
|
||||
{
|
||||
public override string Name => "TestNotification";
|
||||
public override string Link => "";
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void OnHealthIssue(NzbDrone.Core.HealthCheck.HealthCheck artist)
|
||||
{
|
||||
TestLogger.Info("OnHealthIssue was called");
|
||||
}
|
||||
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
TestLogger.Info("OnApplicationUpdate was called");
|
||||
}
|
||||
}
|
||||
|
||||
private class TestNotificationWithNoEvents : NotificationBase<TestSetting>
|
||||
{
|
||||
public override string Name => "TestNotification";
|
||||
public override string Link => "";
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_support_all_if_implemented()
|
||||
{
|
||||
var notification = new TestNotificationWithAllEvents();
|
||||
|
||||
notification.SupportsOnHealthIssue.Should().BeTrue();
|
||||
notification.SupportsOnApplicationUpdate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_support_none_if_none_are_implemented()
|
||||
{
|
||||
var notification = new TestNotificationWithNoEvents();
|
||||
|
||||
notification.SupportsOnHealthIssue.Should().BeFalse();
|
||||
notification.SupportsOnApplicationUpdate.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DateTimeRoutinesFixture : CoreTest
|
||||
{
|
||||
public static IEnumerable DateTimeTestCases
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new TestCaseData(@"Member since: 10-Feb-2008").Returns(new DateTime(2008, 2, 10, 0, 0, 0));
|
||||
yield return new TestCaseData(@"Last Update: 18:16 11 Feb '08 ").Returns(new DateTime(2008, 2, 11, 18, 16, 0));
|
||||
yield return new TestCaseData(@"date Tue, Feb 10, 2008 at 11:06 AM").Returns(new DateTime(2008, 2, 10, 11, 06, 0));
|
||||
yield return new TestCaseData(@"see at 12/31/2007 14:16:32").Returns(new DateTime(2007, 12, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"sack finish 14:16:32 November 15 2008, 1-144 app").Returns(new DateTime(2008, 11, 15, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Genesis Message - Wed 04 Feb 08 - 19:40").Returns(new DateTime(2008, 2, 4, 19, 40, 0));
|
||||
yield return new TestCaseData(@"The day 07/31/07 14:16:32 is ").Returns(new DateTime(2007, 7, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Shipping is on us until December 24, 2008 within the U.S. ").Returns(new DateTime(2008, 12, 24, 0, 0, 0));
|
||||
yield return new TestCaseData(@" 2008 within the U.S. at 14:16:32").Returns(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 14, 16, 32));
|
||||
yield return new TestCaseData(@"5th November, 1994, 8:15:30 pm").Returns(new DateTime(1994, 11, 5, 20, 15, 30));
|
||||
yield return new TestCaseData(@"7 boxes January 31 , 14:16:32.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"the blue sky of Sept 30th 2008 14:16:32").Returns(new DateTime(2008, 9, 30, 14, 16, 32));
|
||||
yield return new TestCaseData(@" e.g. 1997-07-16T19:20:30+01:00").Returns(new DateTime(1997, 7, 16, 19, 20, 30));
|
||||
yield return new TestCaseData(@"Apr 1st, 2008 14:16:32 tufa 6767").Returns(new DateTime(2008, 4, 1, 14, 16, 32));
|
||||
yield return new TestCaseData(@"wait for 07/31/07 14:16:32").Returns(new DateTime(2007, 7, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"later 12.31.08 and before 1.01.09").Returns(new DateTime(2008, 12, 31, 0, 0, 0));
|
||||
yield return new TestCaseData(@"Expires: Sept 30th 2008 14:16:32").Returns(new DateTime(2008, 9, 30, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Offer expires Apr 1st, 2007, 14:16:32").Returns(new DateTime(2007, 4, 1, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Expires 14:16:32 January 31.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Expires 14:16:32 January 31-st.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32));
|
||||
yield return new TestCaseData(@"Expires 23rd January 2010.").Returns(new DateTime(2010, 1, 23, 0, 0, 0));
|
||||
yield return new TestCaseData(@"Expires January 22nd, 2010.").Returns(new DateTime(2010, 1, 22, 0, 0, 0));
|
||||
yield return new TestCaseData(@"Expires DEC 22, 2010.").Returns(new DateTime(2010, 12, 22, 0, 0, 0));
|
||||
yield return new TestCaseData(@"Version: 1.0.0.692 6/1/2010 2:28:04 AM ").Returns(new DateTime(2010, 6, 1, 2, 28, 4));
|
||||
yield return new TestCaseData(@"Version: 1.0.0.692 04/21/11 12:30am ").Returns(new DateTime(2011, 4, 21, 00, 30, 00));
|
||||
yield return new TestCaseData(@"Version: 1.0.0.692 04/21/11 12:30pm ").Returns(new DateTime(2011, 4, 21, 12, 30, 00));
|
||||
yield return new TestCaseData(@"Version: Thu Aug 06 22:32:15 MDT 2009 ").Returns(new DateTime(2009, 8, 6, 22, 32, 15));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCaseSource("DateTimeTestCases")]
|
||||
public DateTime should_parse_date(string date)
|
||||
{
|
||||
DateTimeRoutines.TryParseDateOrTime(date, DateTimeRoutines.DateTimeFormat.USDate, out var parsedDateTime);
|
||||
|
||||
return parsedDateTime.DateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Section { get; set; }
|
||||
public HiddenType Hidden { get; set; }
|
||||
public PrivacyLevel Privacy { get; set; }
|
||||
public string Placeholder { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||
|
||||
153
src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs
Normal file
153
src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarian : ApplicationBase<LazyLibrarianSettings>
|
||||
{
|
||||
public override string Name => "LazyLibrarian";
|
||||
|
||||
private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
|
||||
: base(appIndexerMapService, logger)
|
||||
{
|
||||
_lazyLibrarianV1Proxy = lazyLibrarianV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_lazyLibrarianV1Proxy.TestConnection(Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to LazyLibrarian"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public override List<AppIndexerMap> GetIndexerMappings()
|
||||
{
|
||||
var indexers = _lazyLibrarianV1Proxy.GetIndexers(Settings);
|
||||
|
||||
var mappings = new List<AppIndexerMap>();
|
||||
|
||||
foreach (var indexer in indexers)
|
||||
{
|
||||
if (indexer.Apikey == _configFileProvider.ApiKey)
|
||||
{
|
||||
var match = AppIndexerRegex.Match(indexer.Host);
|
||||
|
||||
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
|
||||
{
|
||||
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
|
||||
mappings.Add(new AppIndexerMap { RemoteIndexerName = $"{indexer.Type},{indexer.Name}", IndexerId = indexerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol);
|
||||
|
||||
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
|
||||
}
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
{
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
|
||||
|
||||
if (indexerMapping != null)
|
||||
{
|
||||
//Remove Indexer remotely and then remove the mapping
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
_lazyLibrarianV1Proxy.RemoveIndexer(indexerProps[1], (LazyLibrarianProviderType)Enum.Parse(typeof(LazyLibrarianProviderType), indexerProps[0]), Settings);
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
|
||||
|
||||
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol, indexerProps[1]);
|
||||
|
||||
//Use the old remote id to find the indexer on LazyLibrarian incase the update was from a name change in Prowlarr
|
||||
var remoteIndexer = _lazyLibrarianV1Proxy.GetIndexer(indexerProps[1], lazyLibrarianIndexer.Type, Settings);
|
||||
|
||||
if (remoteIndexer != null)
|
||||
{
|
||||
_logger.Debug("Remote indexer found, syncing with current settings");
|
||||
|
||||
if (!lazyLibrarianIndexer.Equals(remoteIndexer))
|
||||
{
|
||||
_lazyLibrarianV1Proxy.UpdateIndexer(lazyLibrarianIndexer, Settings);
|
||||
indexerMapping.RemoteIndexerName = $"{lazyLibrarianIndexer.Type},{lazyLibrarianIndexer.Altername}";
|
||||
_appIndexerMapService.Update(indexerMapping);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_appIndexerMapService.Delete(indexerMapping.Id);
|
||||
|
||||
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
|
||||
{
|
||||
_logger.Debug("Remote indexer not found, re-adding {0} to LazyLibrarian", indexer.Name);
|
||||
var newRemoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
|
||||
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{newRemoteIndexer.Type},{newRemoteIndexer.Name}" });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Remote indexer not found for {0}, skipping re-add to LazyLibrarian due to indexer capabilities", indexer.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
|
||||
{
|
||||
var schema = protocol == DownloadProtocol.Usenet ? LazyLibrarianProviderType.Newznab : LazyLibrarianProviderType.Torznab;
|
||||
|
||||
var lazyLibrarianIndexer = new LazyLibrarianIndexer
|
||||
{
|
||||
Name = originalName ?? $"{indexer.Name} (Prowlarr)",
|
||||
Altername = $"{indexer.Name} (Prowlarr)",
|
||||
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
|
||||
Apikey = _configFileProvider.ApiKey,
|
||||
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
|
||||
Enabled = indexer.Enable,
|
||||
Type = schema,
|
||||
};
|
||||
|
||||
return lazyLibrarianIndexer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarianError
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarianException : NzbDroneException
|
||||
{
|
||||
public LazyLibrarianException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public LazyLibrarianException(string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public LazyLibrarianException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarianIndexerResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public LazyLibrarianIndexerData Data { get; set; }
|
||||
public LazyLibrarianError Error { get; set; }
|
||||
}
|
||||
|
||||
public class LazyLibrarianIndexerData
|
||||
{
|
||||
public List<LazyLibrarianIndexer> Torznabs { get; set; }
|
||||
public List<LazyLibrarianIndexer> Newznabs { get; set; }
|
||||
}
|
||||
|
||||
public enum LazyLibrarianProviderType
|
||||
{
|
||||
Newznab,
|
||||
Torznab
|
||||
}
|
||||
|
||||
public class LazyLibrarianIndexer
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Host { get; set; }
|
||||
public string Apikey { get; set; }
|
||||
public string Categories { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Altername { get; set; }
|
||||
public LazyLibrarianProviderType Type { get; set; }
|
||||
|
||||
public bool Equals(LazyLibrarianIndexer other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return other.Host == Host &&
|
||||
other.Apikey == Apikey &&
|
||||
other.Name == Name &&
|
||||
other.Categories == Categories &&
|
||||
other.Enabled == Enabled &&
|
||||
other.Altername == Altername;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarianSettingsValidator : AbstractValidator<LazyLibrarianSettings>
|
||||
{
|
||||
public LazyLibrarianSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).IsValidUrl();
|
||||
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
RuleFor(c => c.SyncCategories).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class LazyLibrarianSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly LazyLibrarianSettingsValidator Validator = new LazyLibrarianSettingsValidator();
|
||||
|
||||
public LazyLibrarianSettings()
|
||||
{
|
||||
SyncCategories = new[]
|
||||
{
|
||||
NewznabStandardCategory.AudioAudiobook.Id,
|
||||
NewznabStandardCategory.Books.Id,
|
||||
NewznabStandardCategory.BooksComics.Id,
|
||||
NewznabStandardCategory.BooksEBook.Id,
|
||||
NewznabStandardCategory.BooksForeign.Id,
|
||||
NewznabStandardCategory.BooksMags.Id,
|
||||
NewznabStandardCategory.BooksOther.Id,
|
||||
NewznabStandardCategory.BooksTechnical.Id,
|
||||
};
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as LazyLibrarian sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "LazyLibrarian Server", HelpText = "URL used to connect to LazyLibrarian server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:5299")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by LazyLibrarian in Settings/Web Interface")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public class LazyLibrarianStatus
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public LazyLibrarianError Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Applications.LazyLibrarian
|
||||
{
|
||||
public interface ILazyLibrarianV1Proxy
|
||||
{
|
||||
LazyLibrarianIndexer AddIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings);
|
||||
List<LazyLibrarianIndexer> GetIndexers(LazyLibrarianSettings settings);
|
||||
LazyLibrarianIndexer GetIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings);
|
||||
void RemoveIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings);
|
||||
LazyLibrarianIndexer UpdateIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings);
|
||||
ValidationFailure TestConnection(LazyLibrarianSettings settings);
|
||||
}
|
||||
|
||||
public class LazyLibrarianV1Proxy : ILazyLibrarianV1Proxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public LazyLibrarianV1Proxy(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public LazyLibrarianStatus GetStatus(LazyLibrarianSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api", "getVersion", HttpMethod.GET);
|
||||
return Execute<LazyLibrarianStatus>(request);
|
||||
}
|
||||
|
||||
public List<LazyLibrarianIndexer> GetIndexers(LazyLibrarianSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api", "listNabProviders", HttpMethod.GET);
|
||||
|
||||
var response = Execute<LazyLibrarianIndexerResponse>(request);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new LazyLibrarianException(string.Format("LazyLibrarian Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
|
||||
}
|
||||
|
||||
var indexers = new List<LazyLibrarianIndexer>();
|
||||
|
||||
var torIndexers = response.Data.Torznabs;
|
||||
torIndexers.ForEach(i => i.Type = LazyLibrarianProviderType.Torznab);
|
||||
|
||||
var nzbIndexers = response.Data.Newznabs;
|
||||
nzbIndexers.ForEach(i => i.Type = LazyLibrarianProviderType.Newznab);
|
||||
|
||||
indexers.AddRange(torIndexers);
|
||||
indexers.AddRange(nzbIndexers);
|
||||
indexers.ForEach(i => i.Altername = i.Name);
|
||||
|
||||
return indexers;
|
||||
}
|
||||
|
||||
public LazyLibrarianIndexer GetIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings)
|
||||
{
|
||||
var indexers = GetIndexers(settings);
|
||||
|
||||
return indexers.SingleOrDefault(i => i.Name == indexerName && i.Type == indexerType);
|
||||
}
|
||||
|
||||
public void RemoveIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "name", indexerName },
|
||||
{ "providertype", indexerType.ToString().ToLower() }
|
||||
};
|
||||
|
||||
var request = BuildRequest(settings, "/api", "delProvider", HttpMethod.GET, parameters);
|
||||
CheckForError(Execute<LazyLibrarianStatus>(request));
|
||||
}
|
||||
|
||||
public LazyLibrarianIndexer AddIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "name", indexer.Name },
|
||||
{ "providertype", indexer.Type.ToString().ToLower() },
|
||||
{ "host", indexer.Host },
|
||||
{ "prov_apikey", indexer.Apikey },
|
||||
{ "enabled", indexer.Enabled.ToString().ToLower() },
|
||||
{ "categories", indexer.Categories }
|
||||
};
|
||||
|
||||
var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.GET, parameters);
|
||||
CheckForError(Execute<LazyLibrarianStatus>(request));
|
||||
return indexer;
|
||||
}
|
||||
|
||||
public LazyLibrarianIndexer UpdateIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "name", indexer.Name },
|
||||
{ "providertype", indexer.Type.ToString().ToLower() },
|
||||
{ "host", indexer.Host },
|
||||
{ "prov_apikey", indexer.Apikey },
|
||||
{ "enabled", indexer.Enabled.ToString().ToLower() },
|
||||
{ "categories", indexer.Categories },
|
||||
{ "altername", indexer.Altername }
|
||||
};
|
||||
|
||||
var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.GET, parameters);
|
||||
CheckForError(Execute<LazyLibrarianStatus>(request));
|
||||
return indexer;
|
||||
}
|
||||
|
||||
private void CheckForError(LazyLibrarianStatus response)
|
||||
{
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new LazyLibrarianException(string.Format("LazyLibrarian Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public ValidationFailure TestConnection(LazyLibrarianSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = GetStatus(settings);
|
||||
|
||||
if (!status.Success)
|
||||
{
|
||||
return new ValidationFailure("ApiKey", status.Error.Message);
|
||||
}
|
||||
|
||||
var indexers = GetIndexers(settings);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (LazyLibrarianException ex)
|
||||
{
|
||||
_logger.Error(ex, "Connection test failed");
|
||||
return new ValidationFailure("", ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("", "Unable to send test message");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(LazyLibrarianSettings settings, string resource, string command, HttpMethod method, Dictionary<string, string> parameters = null)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(baseUrl).Resource(resource)
|
||||
.AddQueryParam("cmd", command)
|
||||
.AddQueryParam("apikey", settings.ApiKey);
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
requestBuilder.AddQueryParam(param.Key, param.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
request.Headers.ContentType = "application/json";
|
||||
|
||||
request.Method = method;
|
||||
request.AllowAutoRedirect = true;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private TResource Execute<TResource>(HttpRequest request)
|
||||
where TResource : new()
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,13 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public LidarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8686";
|
||||
SyncCategories = new[] { 3000, 3010, 3030, 3040, 3050, 3060 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
|
||||
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "URL used to connect to Lidarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8686")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")]
|
||||
|
||||
@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
|
||||
public MylarSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8090";
|
||||
SyncCategories = new[] { NewznabStandardCategory.BooksComics.Id };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Mylar sees it, including http(s)://, port, and urlbase if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Mylar sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Mylar Server", HelpText = "Mylar server URL, including http(s):// and port if needed")]
|
||||
[FieldDefinition(1, Label = "Mylar Server", HelpText = "URL used to connect to Mylar server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8090")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Mylar in Settings/Web Interface")]
|
||||
|
||||
@@ -133,12 +133,19 @@ namespace NzbDrone.Core.Applications.Mylar
|
||||
{
|
||||
return new ValidationFailure("ApiKey", status.Error.Message);
|
||||
}
|
||||
|
||||
var indexers = GetIndexers(settings);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (MylarException ex)
|
||||
{
|
||||
_logger.Error(ex, "Connection test failed");
|
||||
return new ValidationFailure("", ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
|
||||
@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public RadarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:7878";
|
||||
SyncCategories = new[] { 2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")]
|
||||
[FieldDefinition(1, Label = "Radarr Server", HelpText = "URL used to connect to Radarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:7878")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")]
|
||||
|
||||
@@ -23,15 +23,13 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public ReadarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8787";
|
||||
SyncCategories = new[] { 3030, 7000, 7010, 7020, 7030, 7040, 7050, 7060 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]
|
||||
[FieldDefinition(1, Label = "Readarr Server", HelpText = "URL used to connect to Readarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8787")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")]
|
||||
|
||||
@@ -22,16 +22,14 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public SonarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:8989";
|
||||
SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050 };
|
||||
AnimeSyncCategories = new[] { 5070 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]
|
||||
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "URL used to connect to Sonarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:8989")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Sonarr in Settings/General")]
|
||||
|
||||
@@ -187,9 +187,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()
|
||||
|
||||
@@ -47,6 +47,12 @@ namespace NzbDrone.Core.Configuration
|
||||
string UpdateScriptPath { get; }
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -186,6 +192,12 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false);
|
||||
public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false);
|
||||
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
|
||||
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
||||
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
||||
public int PostgresPort => GetValueInt("PostgresPort", 5436, 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);
|
||||
|
||||
@@ -78,7 +78,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}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,22 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
|
||||
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
||||
if (_database.DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
|
||||
}
|
||||
}
|
||||
|
||||
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
|
||||
{
|
||||
SqlBuilderExtensions.LogQuery(_insertSql, model);
|
||||
var multi = connection.QueryMultiple(_insertSql, model, transaction);
|
||||
var id = (int)multi.Read().First().id;
|
||||
var multiRead = multi.Read();
|
||||
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||
_keyProperty.SetValue(model, id);
|
||||
|
||||
_database.ApplyLazyLoad(model);
|
||||
@@ -287,7 +295,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
conn.Execute($"DELETE FROM [{_table}]");
|
||||
conn.Execute($"DELETE FROM \"{_table}\"");
|
||||
}
|
||||
|
||||
if (vacuum)
|
||||
@@ -346,7 +354,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++)
|
||||
{
|
||||
@@ -414,9 +422,12 @@ namespace NzbDrone.Core.Datastore
|
||||
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
||||
}
|
||||
|
||||
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
|
||||
|
||||
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
||||
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
return queryFunc(builder).ToList();
|
||||
}
|
||||
|
||||
@@ -1,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
|
||||
{
|
||||
@@ -14,10 +16,17 @@ 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());
|
||||
_configFileProvider = configFileProvider;
|
||||
|
||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||
GetConnectionString(appFolderInfo.GetDatabase());
|
||||
|
||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
}
|
||||
|
||||
public string MainDbConnectionString { get; private set; }
|
||||
@@ -48,5 +57,19 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
}
|
||||
|
||||
private string GetPostgresConnectionString(string dbName)
|
||||
{
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
||||
|
||||
connectionBuilder.Database = dbName;
|
||||
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
||||
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
||||
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
||||
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
||||
connectionBuilder.Enlist = false;
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.Datastore
|
||||
IDbConnection OpenConnection();
|
||||
Version Version { get; }
|
||||
int Migration { get; }
|
||||
DatabaseType DatabaseType { get; }
|
||||
void Vacuum();
|
||||
}
|
||||
|
||||
@@ -32,13 +34,44 @@ namespace NzbDrone.Core.Datastore
|
||||
return _datamapperFactory();
|
||||
}
|
||||
|
||||
public DatabaseType DatabaseType
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version Version
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
var version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
string version;
|
||||
|
||||
try
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SHOW server_version");
|
||||
|
||||
//Postgres can return extra info about operating system on version call, ignore this
|
||||
version = Regex.Replace(version, @"\(.*?\)", "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
}
|
||||
|
||||
return new Version(version);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +83,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
return db.QueryFirstOrDefault<int>("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1");
|
||||
return db.QueryFirstOrDefault<int>("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,4 +106,10 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DatabaseType
|
||||
{
|
||||
SQLite,
|
||||
PostgreSQL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@@ -85,10 +88,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;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ 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)
|
||||
|
||||
@@ -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 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();
|
||||
|
||||
@@ -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 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();
|
||||
|
||||
@@ -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';");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
Alter.Table("History")
|
||||
.AddColumn("Successful").AsBoolean().NotNullable().WithDefaultValue(true);
|
||||
|
||||
Execute.Sql("UPDATE History SET Successful = (json_extract(History.Data,'$.successful') == 'True' );");
|
||||
// Postgres added after this, not needed
|
||||
IfDatabase("sqlite").Execute.Sql("UPDATE \"History\" SET \"Successful\" = (json_extract(\"History\".\"Data\",'$.successful') == 'True' );");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT Id, Settings FROM Indexers WHERE Implementation = 'Redacted'";
|
||||
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
using (var updateCmd = conn.CreateCommand())
|
||||
{
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = "UPDATE Indexers SET Settings = ?, ConfigContract = ?, Enable = 0 WHERE Id = ?";
|
||||
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?";
|
||||
updateCmd.AddParameter(settings);
|
||||
updateCmd.AddParameter("RedactedSettings");
|
||||
updateCmd.AddParameter(id);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "Unit3dSettings", Enable = 0 }).Where(new { Implementation = "DesiTorrents" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "Unit3dSettings", Enable = true }).Where(new { Implementation = "DesiTorrents" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(014)]
|
||||
public class add_on_update_to_notifications : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Notifications").AddColumn("OnApplicationUpdate").AsBoolean().WithDefaultValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using FluentMigrator.Model;
|
||||
using FluentMigrator.Runner.Processors.SQLite;
|
||||
|
||||
@@ -66,6 +67,24 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
if (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
|
||||
{
|
||||
if (columnReader.ValueToUpper == "PRIMARY")
|
||||
{
|
||||
columnReader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);
|
||||
if (columnReader.Read() == SqliteSyntaxReader.TokenType.Identifier)
|
||||
{
|
||||
var pk = table.Columns.First(v => v.Name == columnReader.Value);
|
||||
pk.IsPrimaryKey = true;
|
||||
pk.IsNullable = true;
|
||||
pk.IsUnique = true;
|
||||
if (columnReader.Buffer.ToUpperInvariant().Contains("AUTOINCREMENT"))
|
||||
{
|
||||
pk.IsIdentity = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (columnReader.ValueToUpper == "CONSTRAINT" ||
|
||||
columnReader.ValueToUpper == "PRIMARY" || columnReader.ValueToUpper == "UNIQUE" ||
|
||||
columnReader.ValueToUpper == "CHECK" || columnReader.ValueToUpper == "FOREIGN")
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class TableMapper
|
||||
{
|
||||
private readonly HashSet<string> _allowedOrderBy = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public TableMapper()
|
||||
{
|
||||
IgnoreList = new Dictionary<Type, List<PropertyInfo>>();
|
||||
@@ -27,12 +29,12 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
if (IgnoreList.TryGetValue(type, out var list))
|
||||
{
|
||||
return new ColumnMapper<TEntity>(list, LazyLoadList[type]);
|
||||
return new ColumnMapper<TEntity>(list, LazyLoadList[type], _allowedOrderBy);
|
||||
}
|
||||
|
||||
IgnoreList[type] = new List<PropertyInfo>();
|
||||
LazyLoadList[type] = new List<LazyLoadedProperty>();
|
||||
return new ColumnMapper<TEntity>(IgnoreList[type], LazyLoadList[type]);
|
||||
return new ColumnMapper<TEntity>(IgnoreList[type], LazyLoadList[type], _allowedOrderBy);
|
||||
}
|
||||
|
||||
public List<PropertyInfo> ExcludeProperties(Type x)
|
||||
@@ -40,6 +42,64 @@ namespace NzbDrone.Core.Datastore
|
||||
return IgnoreList.ContainsKey(x) ? IgnoreList[x] : new List<PropertyInfo>();
|
||||
}
|
||||
|
||||
public bool IsValidSortKey(string sortKey)
|
||||
{
|
||||
string table = null;
|
||||
|
||||
if (sortKey.Contains('.'))
|
||||
{
|
||||
var split = sortKey.Split('.');
|
||||
if (split.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
table = split[0];
|
||||
sortKey = split[1];
|
||||
}
|
||||
|
||||
if (table != null && !TableMap.Values.Contains(table, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_allowedOrderBy.Contains(sortKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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 string TableNameMapping(Type x)
|
||||
{
|
||||
return TableMap.ContainsKey(x) ? TableMap[x] : null;
|
||||
@@ -47,17 +107,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**/";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,17 +132,20 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
private readonly List<PropertyInfo> _ignoreList;
|
||||
private readonly List<LazyLoadedProperty> _lazyLoadList;
|
||||
private readonly HashSet<string> _allowedOrderBy;
|
||||
|
||||
public ColumnMapper(List<PropertyInfo> ignoreList, List<LazyLoadedProperty> lazyLoadList)
|
||||
public ColumnMapper(List<PropertyInfo> ignoreList, List<LazyLoadedProperty> lazyLoadList, HashSet<string> allowedOrderBy)
|
||||
{
|
||||
_ignoreList = ignoreList;
|
||||
_lazyLoadList = lazyLoadList;
|
||||
_allowedOrderBy = allowedOrderBy;
|
||||
}
|
||||
|
||||
public ColumnMapper<T> AutoMapPropertiesWhere(Func<PropertyInfo, bool> predicate)
|
||||
{
|
||||
var properties = typeof(T).GetProperties();
|
||||
_ignoreList.AddRange(properties.Where(x => !predicate(x)));
|
||||
_allowedOrderBy.UnionWith(properties.Where(x => predicate(x)).Select(x => x.Name));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
.Ignore(i => i.SupportsOnHealthIssue);
|
||||
.Ignore(i => i.SupportsOnHealthIssue)
|
||||
.Ignore(i => i.SupportsOnApplicationUpdate);
|
||||
|
||||
Mapper.Entity<IndexerProxyDefinition>("IndexerProxies").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName);
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
|
||||
void DeleteForIndexers(List<int> indexerIds);
|
||||
History MostRecentForIndexer(int indexerId);
|
||||
List<History> Between(DateTime start, DateTime end);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
void Cleanup(int days);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
@@ -78,6 +79,13 @@ namespace NzbDrone.Core.History
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<History> Between(DateTime start, DateTime end)
|
||||
{
|
||||
var builder = Builder().Where<History>(x => x.Date >= start && x.Date <= end);
|
||||
|
||||
return Query(builder).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
|
||||
public List<History> Since(DateTime date, HistoryEventType? eventType)
|
||||
{
|
||||
var builder = Builder().Where<History>(x => x.Date >= date);
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> FindByDownloadId(string downloadId);
|
||||
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
|
||||
void UpdateMany(List<History> toUpdate);
|
||||
List<History> Between(DateTime start, DateTime end);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
}
|
||||
@@ -87,6 +88,11 @@ namespace NzbDrone.Core.History
|
||||
_historyRepository.UpdateMany(toUpdate);
|
||||
}
|
||||
|
||||
public List<History> Between(DateTime start, DateTime end)
|
||||
{
|
||||
return _historyRepository.Between(start, end);
|
||||
}
|
||||
|
||||
public List<History> Since(DateTime date, HistoryEventType? eventType)
|
||||
{
|
||||
return _historyRepository.Since(date, eventType);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
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)");
|
||||
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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,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 Indexers
|
||||
ON History.IndexerId = Indexers.Id
|
||||
WHERE Indexers.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""History"".""IndexerId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,22 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
|
||||
if (usedTags.Length > 0)
|
||||
{
|
||||
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
|
||||
|
||||
mapper.Execute($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})");
|
||||
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute($"DELETE FROM \"Tags\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] GetUsedTags(string table, IDbConnection mapper)
|
||||
{
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'")
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]'")
|
||||
.SelectMany(x => x)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -26,9 +26,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"UPDATE ScheduledTasks
|
||||
SET LastExecution = @time
|
||||
WHERE LastExecution > @time",
|
||||
mapper.Execute(@"UPDATE ""ScheduledTasks""
|
||||
SET ""LastExecution"" = @time
|
||||
WHERE ""LastExecution"" > @time",
|
||||
new { time = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Localization;
|
||||
@@ -16,17 +18,27 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
|
||||
{
|
||||
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx" };
|
||||
private readonly ICached<string> _cache;
|
||||
|
||||
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService)
|
||||
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
|
||||
: base(cloudRequestBuilder, httpClient, logger, localizationService)
|
||||
{
|
||||
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
|
||||
}
|
||||
|
||||
public override string Name => "FlareSolverr";
|
||||
|
||||
public override HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
//Try original request first, detect CF in post response
|
||||
//Try original request first, ignore errors, detect CF in post response
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
//Inject UA if not present
|
||||
if (_cache.Find(request.Url.Host).IsNotNullOrWhiteSpace() && request.Headers.UserAgent.IsNullOrWhiteSpace())
|
||||
{
|
||||
request.Headers.UserAgent = _cache.Find(request.Url.Host);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -49,18 +61,18 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
|
||||
result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content);
|
||||
|
||||
var cookieCollection = new CookieCollection();
|
||||
var responseHeader = new HttpHeader();
|
||||
var newRequest = response.Request;
|
||||
|
||||
foreach (var cookie in result.Solution.Cookies)
|
||||
{
|
||||
cookieCollection.Add(cookie.ToCookieObj());
|
||||
}
|
||||
//Cache the user-agent so we can inject it in next request to avoid re-solve
|
||||
_cache.Set(response.Request.Url.Host, result.Solution.UserAgent);
|
||||
newRequest.Headers.UserAgent = result.Solution.UserAgent;
|
||||
|
||||
//Build new response with FS Cookie and Site Response
|
||||
var newResponse = new HttpResponse(response.Request, responseHeader, cookieCollection, result.Solution.Response);
|
||||
InjectCookies(newRequest, result);
|
||||
|
||||
return newResponse;
|
||||
//Request again with User-Agent and Cookies from Flaresolvrr
|
||||
var finalResponse = _httpClient.Execute(newRequest);
|
||||
|
||||
return finalResponse;
|
||||
}
|
||||
|
||||
private static bool IsCloudflareProtected(HttpResponse response)
|
||||
@@ -77,13 +89,31 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
return false;
|
||||
}
|
||||
|
||||
private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse)
|
||||
{
|
||||
var rCookies = flareSolverrResponse.Solution.Cookies;
|
||||
|
||||
if (!rCookies.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rCookiesList = rCookies.Select(x => x.Name).ToList();
|
||||
|
||||
foreach (var rCookie in rCookies)
|
||||
{
|
||||
request.Cookies.Remove(rCookie.Name);
|
||||
request.Cookies.Add(rCookie.Name, rCookie.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequest GenerateFlareSolverrRequest(HttpRequest request)
|
||||
{
|
||||
FlareSolverrRequest req;
|
||||
|
||||
var url = request.Url.ToString();
|
||||
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
|
||||
var maxTimeout = 60000;
|
||||
var maxTimeout = Settings.RequestTimeout * 1000;
|
||||
|
||||
if (request.Method == HttpMethod.GET)
|
||||
{
|
||||
@@ -149,7 +179,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var request = PreRequest(_cloudRequestBuilder.Create()
|
||||
var request = GenerateFlareSolverrRequest(_cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build());
|
||||
|
||||
@@ -157,12 +187,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode)));
|
||||
}
|
||||
|
||||
var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(response.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentValidation;
|
||||
using NLog.Config;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
@@ -9,6 +10,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public FlareSolverrSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).NotEmpty();
|
||||
RuleFor(c => c.RequestTimeout).InclusiveBetween(1, 180);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +21,15 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public FlareSolverrSettings()
|
||||
{
|
||||
Host = "http://localhost:8191/";
|
||||
RequestTimeout = 60;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Request Timeout", Advanced = true, HelpText = "FlareSolverr maxTimeout Request Parameter", Unit = "seconds")]
|
||||
public int RequestTimeout { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -31,12 +31,6 @@ namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var addresses = Dns.GetHostAddresses(Settings.Host);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), addresses)));
|
||||
}
|
||||
|
||||
var request = PreRequest(_cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build());
|
||||
@@ -55,7 +49,7 @@ namespace NzbDrone.Core.IndexerProxies
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", request.Url)));
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message)));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public int? TmdbId { get; set; }
|
||||
public int? TraktId { get; set; }
|
||||
public int? Year { get; set; }
|
||||
public string Genre { get; set; }
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
@@ -64,6 +65,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
builder = builder.Append($" TraktId:[{TraktId}]");
|
||||
}
|
||||
|
||||
if (Genre.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
builder = builder.Append($" Genre:[{Genre}]");
|
||||
}
|
||||
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public string Album { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Genre { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
public override bool RssSearch
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
public int? RId { get; set; }
|
||||
public int? TvMazeId { get; set; }
|
||||
public int? TraktId { get; set; }
|
||||
public int? TmdbId { get; set; }
|
||||
|
||||
public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim();
|
||||
public string EpisodeSearchString => GetEpisodeSearchString();
|
||||
@@ -74,6 +75,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
builder.Append($" TraktId:[{TraktId}]");
|
||||
}
|
||||
|
||||
if (TmdbId.HasValue)
|
||||
{
|
||||
builder.Append($" TmdbId:[{TmdbId}]");
|
||||
}
|
||||
|
||||
builder = builder.Append(searchEpisodeTerm);
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
|
||||
@@ -89,15 +89,21 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")),
|
||||
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
|
||||
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
|
||||
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c.Id, protocol),
|
||||
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol),
|
||||
GetNabElement("rageid", r.TvRageId, protocol),
|
||||
GetNabElement("thetvdb", r.TvdbId, protocol),
|
||||
GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
GetNabElement("tmdb", r.TmdbId, protocol),
|
||||
GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
GetNabElement("seeders", t.Seeders, protocol),
|
||||
GetNabElement("files", r.Files, protocol),
|
||||
GetNabElement("grabs", r.Grabs, protocol),
|
||||
GetNabElement("peers", t.Peers, protocol),
|
||||
GetNabElement("infohash", RemoveInvalidXMLChars(r.Guid), protocol),
|
||||
GetNabElement("author", RemoveInvalidXMLChars(r.Author), protocol),
|
||||
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
|
||||
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol),
|
||||
GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol),
|
||||
GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol),
|
||||
GetNabElement("minimumratio", t.MinimumRatio, protocol),
|
||||
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),
|
||||
GetNabElement("downloadvolumefactor", t.DownloadVolumeFactor, protocol),
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
searchSpec.TmdbId = request.tmdbid;
|
||||
searchSpec.TraktId = request.traktid;
|
||||
searchSpec.Year = request.year;
|
||||
searchSpec.Genre = request.genre;
|
||||
|
||||
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
|
||||
}
|
||||
@@ -71,6 +72,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
searchSpec.Artist = request.artist;
|
||||
searchSpec.Album = request.album;
|
||||
searchSpec.Label = request.label;
|
||||
searchSpec.Genre = request.genre;
|
||||
searchSpec.Year = request.year;
|
||||
|
||||
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
|
||||
}
|
||||
@@ -84,6 +87,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
searchSpec.TvdbId = request.tvdbid;
|
||||
searchSpec.ImdbId = request.imdbid;
|
||||
searchSpec.TraktId = request.traktid;
|
||||
searchSpec.TmdbId = request.tmdbid;
|
||||
searchSpec.RId = request.rid;
|
||||
searchSpec.TvMazeId = request.tvmazeid;
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
public class CombinedStatistics
|
||||
{
|
||||
public List<IndexerStatistics> IndexerStatistics { get; set; }
|
||||
public List<UserAgentStatistics> UserAgentStatistics { get; set; }
|
||||
public List<HostStatistics> HostStatistics { get; set; }
|
||||
}
|
||||
|
||||
public class IndexerStatistics : ResultSet
|
||||
{
|
||||
public int IndexerId { get; set; }
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
public interface IIndexerStatisticsRepository
|
||||
{
|
||||
List<IndexerStatistics> IndexerStatistics();
|
||||
List<UserAgentStatistics> UserAgentStatistics();
|
||||
List<HostStatistics> HostStatistics();
|
||||
}
|
||||
|
||||
public class IndexerStatisticsRepository : IIndexerStatisticsRepository
|
||||
{
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM History /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
|
||||
private readonly IMainDatabase _database;
|
||||
|
||||
public IndexerStatisticsRepository(IMainDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public List<IndexerStatistics> IndexerStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return Query(IndexerBuilder());
|
||||
}
|
||||
|
||||
public List<UserAgentStatistics> UserAgentStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return UserAgentQuery(UserAgentBuilder());
|
||||
}
|
||||
|
||||
public List<HostStatistics> HostStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return HostQuery(HostBuilder());
|
||||
}
|
||||
|
||||
private List<IndexerStatistics> Query(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<IndexerStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<UserAgentStatistics> UserAgentQuery(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<UserAgentStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<HostStatistics> HostQuery(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<HostStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private SqlBuilder IndexerBuilder() => new SqlBuilder()
|
||||
.Select(@"Indexers.Id AS IndexerId,
|
||||
Indexers.Name AS IndexerName,
|
||||
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 2 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedQueries,
|
||||
SUM(CASE WHEN EventType == 3 then 1 else 0 end) AS NumberOfRssQueries,
|
||||
SUM(CASE WHEN EventType == 3 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedRssQueries,
|
||||
SUM(CASE WHEN EventType == 4 then 1 else 0 end) AS NumberOfAuthQueries,
|
||||
SUM(CASE WHEN EventType == 4 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedAuthQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs,
|
||||
SUM(CASE WHEN EventType == 1 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedGrabs,
|
||||
AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime")
|
||||
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
|
||||
.GroupBy<IndexerDefinition>(x => x.Id);
|
||||
|
||||
private SqlBuilder UserAgentBuilder() => new SqlBuilder()
|
||||
.Select(@"json_extract(History.Data,'$.source') AS UserAgent,
|
||||
SUM(CASE WHEN EventType == 2 OR EventType == 3 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
|
||||
.GroupBy("UserAgent");
|
||||
|
||||
private SqlBuilder HostBuilder() => new SqlBuilder()
|
||||
.Select(@"json_extract(History.Data,'$.host') AS Host,
|
||||
SUM(CASE WHEN EventType == 2 OR EventType == 3 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
|
||||
.GroupBy("Host");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user