Compare commits

...

49 Commits

Author SHA1 Message Date
bakerboy448
2eb8f0740f CHANGELOG-v0.4.4.1947 2022-08-16 09:00:54 -05:00
bakerboy448
e5176e79d6 CHANGELOG-v0.4.3.1921 2022-08-16 09:00:54 -05:00
bakerboy448
0c78258e06 CHANGELOG-v0.4.2.1879 2022-08-16 09:00:54 -05:00
bakerboy448
16641b8006 CHANGELOG-v0.3.0.1730 2022-08-16 09:00:54 -05:00
bakerboy448
58576a5577 CHANGELOG-v0.2.0.1678 2022-08-16 09:00:54 -05:00
bakerboy448
2f80678a8b [changelog] update commentary for donate of bitcoin and other fixes 2022-08-16 09:00:54 -05:00
bakerboy448
b14727580d generate CHANGELOG-v0.2.0.1448 2022-08-16 09:00:54 -05:00
bakerboy448
c255085628 create [changelog-post] script to generate Release Announcements 2022-08-16 09:00:54 -05:00
Qstick
6446528022 Bump version to 0.4.5 2022-08-15 22:40:22 -05:00
Weblate
7f63757e06 Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 100.0% (462 of 462 strings)

Co-authored-by: Jessie <1355239678@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-08-14 23:40:16 -05:00
Qstick
b5d789df3a Fixed: Correctly persist FlareSolverr Cookies to ensure it doesn't run on every request 2022-08-13 17:30:25 -05:00
Qstick
4473551182 Fixed: Correctly use FlareSolverr User Agent 2022-08-13 17:27:58 -05:00
Qstick
fd88f44865 Remove duplicate package NLog.Extensions in Prowlarr.Common 2022-08-13 16:41:09 -05:00
bakerboy448
69b8be5b67 Fixed: (Cardigann) fix imatch for rows
based on jackett 9768fd288ba299f8a2d1aada1a539d156e7e97b9
2022-08-11 21:32:53 -05:00
ta264
fbde3fe2cd Support for digest auth with HttpRequests
Changes from de243991dd78c0fa4cfd2b629839874bbd8f2650 missed b7b5a6e7e1
2022-08-05 16:09:27 +01:00
bakerboy448
f9e2c5b673 Fixed: (Cardigann) Genre is optional
Fixed: (Cardigann) Expand Genre Validate characters
2022-08-02 23:38:41 -05:00
bakerboy448
5c5dfbb66b Fixed: (Cardigann) Genre Parsing
New: (Cardigann) Add Validate Field Filter

v7
2022-08-02 23:38:41 -05:00
Servarr
2db24d454e Automated API Docs update 2022-07-30 16:22:01 -05:00
Qstick
cb35a3948e Fixed: (Cardigann) Genre Parsing for Releases 2022-07-30 00:07:11 -04:00
Qstick
8c314439cd Fixed: (Cardigann) messy row strdump 2022-07-30 00:07:11 -04:00
Qstick
ee6467073f New: (Cardigann) Additional query support
v7
2022-07-30 00:07:11 -04:00
Qstick
6412048eb9 Bump version to 0.4.4 2022-07-29 12:19:39 -05:00
Qstick
efffeebe7c Fixed: (GazelleGames) Use API instead of scraping 2022-07-29 00:35:14 -04:00
Weblate
1d25a643f9 Translated using Weblate (Hungarian)
Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (462 of 462 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-07-28 14:31:11 -04:00
Servarr
60f48e3a94 Automated API Docs update 2022-07-24 20:45:44 -04:00
Qstick
60f8778305 New: Search by DoubanId 2022-07-24 19:22:06 -05:00
Ben Weidenhofer
d5088cf472 Fixed: UI Typos (#1072)
Updated localization file to provide a few spelling and grammatical corrections.
2022-07-22 07:42:01 -05:00
Weblate
215c87a099 Translated using Weblate (Chinese (Traditional) (zh_TW))
Currently translated at 2.8% (13 of 462 strings)

Translated using Weblate (Catalan)

Currently translated at 66.4% (307 of 462 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN))

Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (French)

Currently translated at 95.4% (441 of 462 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (462 of 462 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: beefnoodle <acer.wang@protonmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-07-22 00:20:53 -04:00
Qstick
32ca2d1720 Update README.md 2022-07-17 19:48:32 -05:00
Servarr
8baf1b533b Automated API Docs update 2022-07-17 19:46:40 -05:00
Qstick
970f80b155 Debounce analytics service 2022-07-17 19:40:40 -05:00
Qstick
b8dd8b1880 Fixed: Set Download and Upload Factors from Generic Torznab 2022-07-17 14:52:21 -05:00
Weblate
f607347bd7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 24.6% (111 of 451 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN))

Currently translated at 99.5% (449 of 451 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (451 of 451 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (451 of 451 strings)

Translated using Weblate (Portuguese)

Currently translated at 80.0% (361 of 451 strings)

Translated using Weblate (Polish)

Currently translated at 75.3% (340 of 451 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (451 of 451 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (451 of 451 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (450 of 451 strings)

Translated using Weblate (Hebrew)

Currently translated at 75.1% (339 of 451 strings)

Translated using Weblate (Finnish)

Currently translated at 99.7% (450 of 451 strings)

Translated using Weblate (German)

Currently translated at 96.2% (434 of 451 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-07-09 00:14:31 -05:00
Qstick
9959a1b5ed Translation Improvements 2022-07-06 19:22:07 -05:00
Qstick
8c10f8b55c Cleanup Language and Localization code 2022-07-06 19:22:07 -05:00
Weblate
cad4f3740b Added translation using Weblate (Lithuanian)
Co-authored-by: Qstick <qstick@gmail.com>
2022-07-05 21:31:56 -05:00
Qstick
f26b0474f5 Fixed: BeyondHD using improperly cased Content-Type header 2022-07-05 07:32:16 -05:00
Qstick
8b8d0b24ae Fix NullRef in Cloudflare detection service 2022-07-05 07:27:48 -05:00
Qstick
4dee1d65d1 New: (AvistaZ) Parse Languages and Subs, pass in response
#694
2022-07-04 22:50:17 -05:00
Qstick
09ed132fe6 Rework Cloudflare Protection Detection 2022-07-04 22:17:39 -05:00
bakerboy448
e85ccd5808 New: (FlareSolverr) DDOS Guard Support
(based on Flaresolverr b62c203f96222602964a291b845e4d16c1a0d43a)
2022-07-04 21:25:25 -05:00
Qstick
37914fb90e Bump Mailkit to 3.3.0 (#1054)
* Bump Mailkit to 3.3.0

* Bump Sentry to 3.19.0
2022-07-04 21:10:19 -05:00
Qstick
f2f6a82cf0 New: Add linux-x86 builds 2022-07-04 20:40:30 -05:00
Qstick
812cf8135a Remove unused XmlRPC dependency 2022-07-04 20:29:17 -05:00
Qstick
e4284d47b0 Fixed: (Cardigann) Use Indexer Encoding for Form Parameters
Fixes #959
2022-07-04 20:11:43 -05:00
Qstick
c53e0054ee Fixed: (Cardigann) Use Session Cookie when making SimpleCaptchaCall
Fixes #262
Fixes #136
Fixes #122
2022-07-04 19:32:55 -05:00
Qstick
ddcef3a99c Fixed: Delete CustomFilters not handled properly 2022-07-04 18:51:52 -05:00
Qstick
b7b5a6e7e1 Modern HTTP Client (#685) 2022-07-04 18:37:31 -05:00
Qstick
593a649045 Bump version to 0.4.3 2022-07-04 18:36:57 -05:00
144 changed files with 55287 additions and 2380 deletions

View File

@@ -27,10 +27,7 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
## Support ## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr) [![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.2' majorVersion: '0.4.5'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -97,15 +97,14 @@ stages:
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled" echo "Extra platforms already enabled"
else else
echo "Enabling BSD support" echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
displayName: Enable FreeBSD Support displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-bsd - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Prowlarr Backend displayName: Build Prowlarr Backend
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -119,24 +118,28 @@ stages:
displayName: Publish Backend displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests artifact: win-x64-tests
displayName: Publish Windows Test Package displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests artifact: linux-x64-tests
displayName: Publish Linux Test Package displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests artifact: linux-musl-x64-tests
displayName: Publish Linux Musl Test Package displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests artifact: freebsd-x64-tests
displayName: Publish FreeBSD Test Package displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests artifact: osx-x64-tests
displayName: Publish MacOS Test Package displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
@@ -239,35 +242,35 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd - bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Prowlarr" -exec chmod a+x {} \; find . -name "Prowlarr" -exec chmod a+x {} \;
find . -name "Prowlarr.Update" -exec chmod a+x {} \; find . -name "Prowlarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows Core zip displayName: Create win-x64 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows x86 Core zip displayName: Create win-x86 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core app displayName: Create osx-x64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar displayName: Create osx-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -275,14 +278,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app displayName: Create osx-arm64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar displayName: Create osx-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -290,7 +293,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Core tar displayName: Create linux-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -298,7 +301,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Musl Core tar displayName: Create linux-musl-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -306,7 +309,15 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -314,7 +325,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar displayName: Create linux-musl-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -322,7 +333,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar displayName: Create linux-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -330,7 +341,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar displayName: Create linux-musl-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -405,22 +416,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'FreebsdCore' testName: 'freebsd-x64'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -439,7 +450,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -469,8 +480,12 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -481,9 +496,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -513,14 +534,14 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost' Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432' Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr' Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
@@ -585,17 +606,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
@@ -612,7 +633,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -654,7 +675,7 @@ stages:
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
@@ -666,7 +687,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'LinuxCoreTests' artifactName: 'linux-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -720,7 +741,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'FreebsdCoreTests' artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -756,11 +777,15 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'linux-musl-x64'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -770,9 +795,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -818,16 +849,19 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: true failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: true failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: true failBuild: true
@@ -845,7 +879,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(osName)CoreTests' artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact

View File

@@ -25,15 +25,22 @@ UpdateVersionNumber()
fi fi
} }
EnableBsdSupport() EnableExtraPlatformsInSDK()
{ {
#todo enable sdk with SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -293,7 +300,8 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_BSD=NO ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -305,8 +313,12 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd) --enable-bsd|--enable-extra-platforms)
ENABLE_BSD=YES ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -350,12 +362,17 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
EnableBsdSupport EnableExtraPlatforms
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -365,9 +382,10 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -406,9 +424,10 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"

View File

@@ -0,0 +1,55 @@
# New Beta Release
Prowlarr v0.2.0.1448 has been released on `develop`
A reminder about the `develop` branch
- **develop - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first. This version will receive updates either weeklyish or bi-weeklyish depending on development.**
# Announcements
- Automated API Documentation Updates recently implemented
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v5](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v5 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- Lidarr v1 coming to `develop` as beta soon^(tm)
- Readarr official beta on `develop` coming soon^(tm) currently dealing with metadata issues
- [Radarr](https://www.reddit.com/r/radarr/comments/sgrsb3/new_stable_release_master_v4045909/) v4.0.4 released to `master` (stable)
- [Radarr Postgres Database Support coming soon (PR#6873)](https://github.com/radarr/radarr/pull/6873)
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
------------
# Release Notes
## v0.2.0.1448 (changes since v0.2.0.1426)
- Sync Indexers on app start, go to http if not sync'd yet
- Misc definition handling improvements
- Fixed: Updated ruTorrent stopped state helptext
- Fixed: Added missing translate for Database
- Fixed: Download limit check was using the query limit instead of the grab limit.
- Other bug fixes and improvements, see github history

View File

@@ -0,0 +1,212 @@
# New Beta Release
Prowlarr v0.2.0.1678 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.**
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.**
# Announcements
- Automated API Documentation Updates recently implemented
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v6](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v6 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- Lidarr v1 coming to `develop` as beta soon^(tm)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- [Readarr official beta on `develop` announced](https://www.reddit.com/r/Readarr/comments/sxvj8y/new_beta_release_develop_v0101248/)
- Radarr Postgres Database Support in `nightly`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.2.0.1678 (changes since v0.2.0.1448)
- Bump moment from 2.29.1 to 2.29.2
- #834 #256 fix for unable to load the Indexes page
- Fix .editorconfig to disallow `this`
- New: MyAnonamouse freeleech support
- Fixed: (BHD) TMDb Parsing Exception
- Fixed: MoreThanTV indexer from browse page layout changes (#922)
- We don't have two Radarrs
- Fix indent from 37c393a659
- Fixed: (HDBits) Treat 403 as Query Limit
- Fixed: (PTP) Treat 403 as Query Limit
- New: (BTN) Rate Limit to 1 Query per 5 Seconds
- Fixed: (BTN) Handle Query Limit Error
- New: (Lidarr/Radarr/Readarr/Sonarr) Improved Errors
- Fixed: Loading old commands from database
- Fixed: Cleanup Temp files after backup creation
- Add Support
- Translated using Weblate (Finnish)
- Fixed: Indexer Infobox Error (#920)
- New: Indexer Description in Add Indexer Modal
- Fixed: Missing Translates
- New: Add Search Capabilities to Indexer API & InfoBox
- Fixed: Update from version in logs
- Automated API Docs update
- Translated using Weblate (Chinese (Simplified) (zh_CN))
- Translated using Weblate (Portuguese (Brazil))
- Fixed: Validation when testing indexers, connections and download clients
- Fixed: Prevent delete of last profile
- New: Load more (page) results on Search UI
- Update webpack packages
- Frontend Package Updates
- Backend Package Updates
- Bump dotnet to 6.0.3
- Translated using Weblate (Spanish)
- Fixed: (Gazelle) Replace Periods for Space in Search Term
- Fixed: (HDSpace) Replace Periods for Space in Search Term
- Fixed: (Anthelion) Replace Periods for Space in Search Term
- Fixed: (Redacted) Map Categories Comedy & E-Learning Videos to 'Other'
- Fixed: No longer require first run as admin on windows (#885)
- Translated using Weblate (Chinese (Simplified) (zh_CN))
- indexer(xthor): moved to YAML definition v5
- Fixed: '/indexers' URL Base breaking UI navigation
- Translated using Weblate (French)
- Fix app settings delete modal not closing and reloading app profiles
- Translated using Weblate (French)
- Bump Swashbuckle to 6.3.0
- Translated using Weblate (Portuguese (Brazil))
- fixup! New: (DanishBytes) Move to YML
- New: (DanishBytes) Move to YML
- Update translation files
- New: (RuTracker.org) add .bet mirror (#876)
- Fixed:(pornolab) language formatting
- New: Housekeeper for ApplicationStatus
- Fixed: Cleanse Tracker api_token from logs
- New: (HDTorrents) Add hd-torrents.org as Url option
- New: (Cardigann) Allow JSON filters
- Fixed: Convert List<HistoryEventTypes> to Int before passing to DB
- Fixed: WhereBuilder for Postgres
- Translated using Weblate (Finnish)
- Fixed: Make authentication cookie name unique to Prowlarr
- Update Categories
- Fixed: Enable response compression over https
- Fixed: (RuTracker) Update Cats
- Fixed: Clarify App Sync Settings (#847)
- Set version header to X-Application-Version (missing hyphen)
- Go to http if def exists on def server
- Fixed: (BHD) Handle API Auth Errors
- Fixed: (Immortalseed) Keywordless Search
- Fixed: (Cardigann) TraktId was mapping to TvRageId
- New: (Cardigann) - Cardigann v4 Support for Genre, Year, and TraktID
- New: (Cardigann) - Cardigann v4 Support for categorydesc
- New: (Cardigann) - Cardigann v4 Add Support for MapTrackerCatDescToNewznab
- New: (Cardigann) - Cardigann v4 Improved Search Logging
- Fixed: Corrected Query Limit and Grab Limit HelpText
- New: (Avistaz) Better error reporting for unauthorized tests
- Fixed: (Cardigann) Requests Failing for Definitions without LegacyLinks
- Bump SharpZipLib from 1.3.1 to 1.3.3 in /src/NzbDrone.Common
- Fixed: (Cardigann) Smarter redirect domain compare
- Fixed: (Cardigann) Treat "Refresh" header as redirect
- Fixed: (Cardigann) Replace legacy links with default link when making requests
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,117 @@
# New Beta Release
Prowlarr v0.3.0.1730 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.
# Announcements
- Automated API Documentation Updates recently implemented
- [*Coming Soon* - Better \*Arr App Sync](https://github.com/Prowlarr/Prowlarr/pull/983)
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v6](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v6 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- Lidarr v1 coming to `develop` as beta soon^(tm)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- [Readarr official beta on `develop` announced](https://www.reddit.com/r/Readarr/comments/sxvj8y/new_beta_release_develop_v0101248/)
- Radarr Postgres Database Support in `nightly`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.3.0.1730 (changes since v0.3.0.1724)
- Fixed: Prevent endless loop when calling IndexerUrls for Torznab
- Deleted translation using Weblate (Chinese (Min Nan))
- Fix some translations
- Other bug fixes and improvements, see GitHub history
## v0.3.0.1724 (changes since v0.2.0.1678)
- Fixed: Prevent endless loop when calling IndexerUrls for Newznab ( #982 )
- Fixed: Default List for Cardigann LegacyLinks
- New: Auto map known legacy BaseUrls for non-Cardigann
- Fixed: (BTN) Move to HTTPS ( #979 )
- Typo for myanonamouse.
- Fixed: (MoreThanTV) Better Response Cleansing ( #928 )
- New: SceneHD Indexer
- Fixed: (MaM) Handle Auth Errors & Session Expiry
- Fixed: Remove Indexer if categories were changed to not include in sync ( #912 )
- Fixed: Sync Indexers on App Edit
- Cleanup Config Values ( #894 )
- Fixed: (Cardigann) Handle json field selector that returns arrays ( #950 )
- New: Schedule refresh and process monitored download tasks at high priority
- Centralise image choice, update to latest images
- Don't return early after re-running checks after startup grace period ( #7147 )
- Fixed: Delay health check notifications on startup
- New: Add date picker for custom filter dates
- Bump Monotorrent to 2.0.5
- Remove old DotNetVersion method and dep
- New: Add backup size information ( #957 )
- Fixed: (BeyondHD) Use TryCoerceInt for tmdbId ( #960 )
- Fixed: (TorrentDay) TV Search returning Series not S/E Results ( #816 )
- Fixed: (CinemaZ and ExoticaZ) Better Log Cleansing
- Fixed: (exoticaz) Category Parsing
- Fixed: (Indexer) HDTorrents search imdbid + season/episode
- Bump version to 0.3.0
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,203 @@
# New Beta Release
Prowlarr v0.4.2.1879 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.
# Announcements
- [Prowlarr Cardigann Definitions Schema Versions and Validations created](https://github.com/Prowlarr/indexers#schemas)
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v7](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v6 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- [Lidarr v1 coming to `master` as recently released](https://www.reddit.com/r/Lidarr/comments/v5fdhi/new_stable_release_master_v1022592/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- [Readarr official beta on `develop` announced](https://www.reddit.com/r/Readarr/comments/sxvj8y/new_beta_release_develop_v0101248/)
- Radarr Postgres Database Support in `nightly` and `develop`
- Prowlarr Postgres Database Support in `nightly` and `develop`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
- \*Arrs Wiki Contributions welcomed and strongly encouraged, simply auth with GitHub on the wiki and update the page
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.4.2.1879 (changes since v0.3.0.1730)
- Don't require user agent for IPTorrents
- Fixed: (Applications) ApiPath can be null from -arr in some cases
- ProtectionService Test Fixture
- Fixed: Lidarr null ref when building indexer for sync
- Fixed: Lidarr null ref when building indexer for sync
- Double MultipartBodyLengthLimit for Backup Restore to 256MB
- Fixed: (IPTorrents) Allow UA override for CF
- Fixed: Log Cleanse Indexer Response Logic and Test Cases
- Fixed: Set update executable permissions correctly
- Fixed: Don't call for server notifications on event driven check
- Update file and folder handling methods from Radarr (#1051)
- Running Integration Tests against Postgres Database (#838)
- Updated NLog Version (#7365)
- Add additional link logging to DownloadService
- Fixed: Correctly remove TorrentParadiseMl
- V6 Cardigann Changes (#1045)
- Sliding expiration for auth cookie and a little clean up
- Bump version to 0.4.2
- Update Sentry to 3.18.0
- Update Swashbuckle to 6.3.1
- Bump dotnet to 6.0.6
- Update AngleSharp to 0.17.0
- Remove ShowRSS C# Implementation
- Swallow HTTP issues on analytics call
- Fix NullRef in analytics service
- Bump version to 0.4.1
- Fix Donation Links
- Fix Tooltips in Dark Theme
- Fixed: (AnimeBytes) Cleanse Passkey from response
- Fixed: (Cardigann) Use variables in keywordsfilters block
- New: (BeyondHD) Better status messages for failures
- Fixed: VIP Healthcheck not triggered for expired indexers
- Use DryIoc for Automoqer, drop Unity dependency
- New: Send description element in nab response
- (Filelist) Update help text for pass key (#1039)
- Fixed: (Exoticaz) Category parsing kills search/feed
- New: (PassThePopcorn) Freeleech only option
- Fixed: (Cardigann) Searching with nab Parent should also use Child categories
- Fixed: Better Cleansing of Tracker Announce Keys
- Automated API Docs update
- Update FE dev dependencies
- Ensure .Mono and .Windows projects have all dependencies in build output
- Fixed: (Gazelle) Parse grouptime as long or date
- Fixed: (ExoticaZ) Category Parsing
- Fixed: Input options background color on mobile
- Fixed: Update AltHub API URL (#1010)
- Automated API Docs update
- New: Dark Theme
- New: Move to CSS Variables for Colorings
- New: Native Theme Engine
- diversify chartcolors for doughnut & stackedbar
- Translated using Weblate (Chinese (Simplified) (zh_CN))
- Catch Postgres log connection errors
- Clean lingering Postgres Connections on Close
- New: Instance name in System/Status API endpoint
- New: Instance name for Page Title
- New: Instance Name used for Syslog
- New: Set Instance Name
- Fixed: Use separate guid for download protection
- Fixed: (RuTracker) Support Raw search from apps
- Fixed: Localization for two part language dialects
- Fixed: (AnimeBytes) Handle series synonyms with commas (#984)
- New: Add Lidarr and Readarr DiscographySeedTime Sync
- New: Add Sonarr SeasonSeedTime Sync
- Fixed: Indexer Tags Helptext
- Automated API Docs update
- New: Seed Settings Sync
- New: Only sync indexers with matching app tags
- Indexer Cleanup
- Bump version to 0.4.0
- Bump version to 0.3.1
- Translated using Weblate (Chinese (Simplified) (zh_CN))
- Fixed: Correct User-Agent api logging
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,110 @@
# New Beta Release
Prowlarr v0.4.3.1921 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.
# Announcements
- [Prowlarr Cardigann Definitions Schema Versions and Validations created](https://github.com/Prowlarr/indexers#schemas)
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v8](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v8 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- [Radarr Develop recently released](https://www.reddit.com/r/radarr/comments/w3kik4/new_release_develop_v4206438/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- Radarr Postgres Database Support in `nightly` and `develop`
- Prowlarr Postgres Database Support in `nightly` and `develop`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
- \*Arrs Wiki Contributions welcomed and strongly encouraged, simply auth with GitHub on the wiki and update the page
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.4.3.1921 (changes since v0.4.2.1879)
- Fixed: (GazelleGames) Use API instead of scraping
- Translated using Weblate (Hungarian)
- Automated API Docs update
- New: Search by DoubanId
- Fixed: UI Typos (#1072)
- Translated using Weblate (Chinese (Traditional) (zh_TW))
- Update README.md
- Automated API Docs update
- Debounce analytics service
- Fixed: Set Download and Upload Factors from Generic Torznab
- Translated using Weblate (Portuguese (Brazil))
- Translation Improvements
- Cleanup Language and Localization code
- Added translation using Weblate (Lithuanian)
- Fixed: BeyondHD using improperly cased Content-Type header
- Fix NullRef in Cloudflare detection service
- New: (AvistaZ) Parse Languages and Subs, pass in response
- Rework Cloudflare Protection Detection
- New: (FlareSolverr) DDOS Guard Support
- Bump Mailkit to 3.3.0 (#1054)
- New: Add linux-x86 builds
- Remove unused XmlRPC dependency
- Fixed: (Cardigann) Use Indexer Encoding for Form Parameters
- Fixed: (Cardigann) Use Session Cookie when making SimpleCaptchaCall
- Fixed: Delete CustomFilters not handled properly
- Modern HTTP Client (#685)
- Bump version to 0.4.3
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,83 @@
# New Beta Release
Prowlarr v0.4.4.1947 has been released on `develop`
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.
# Announcements
- [Prowlarr Cardigann Definitions Schema Versions and Validations created](https://github.com/Prowlarr/indexers#schemas)
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v8](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v8 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png
# Additional Commentary
- [Radarr Develop recently released](https://www.reddit.com/r/radarr/comments/w3kik4/new_release_develop_v4206438/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- Radarr Postgres Database Support in `nightly` and `develop`
- Prowlarr Postgres Database Support in `nightly` and `develop`
- Readarr Postgres Database Support in `nightly`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
- \*Arrs Wiki Contributions welcomed and strongly encouraged, simply auth with GitHub on the wiki and update the page
# Releases
## Native
- [GitHub Releases](https://github.com/Prowlarr/Prowlarr/releases)
- [Wiki Installation Instructions](https://wiki.servarr.com/prowlarr/installation)
## Docker
- [hotio/Prowlarr:testing](https://hotio.dev/containers/prowlarr)
- [lscr.io/linuxserver/Prowlarr:develop](https://docs.linuxserver.io/images/docker-prowlarr)
## NAS Packages
- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally
- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally
------------
# Release Notes
## v0.4.4.1947 (changes since [v0.4.3.1921](https://www.reddit.com/r/prowlarr/comments/wbanhd/new_develop_release_v0431921/))
- Translated using Weblate (Chinese (Simplified) (zh_CN))
- Fixed: Correctly persist FlareSolverr Cookies to ensure it doesn't run on every request
- Fixed: Correctly use FlareSolverr User Agent
- Remove duplicate package NLog.Extensions in Prowlarr.Common
- Fixed: (Cardigann) fix imatch for rows
- Support for digest auth with HttpRequests
- Fixed: (Cardigann) Genre is optional
- Fixed: (Cardigann) Genre Parsing
- Automated API Docs update
- Fixed: (Cardigann) Genre Parsing for Releases
- Fixed: (Cardigann) messy row strdump
- New: (Cardigann) Additional query support
- Bump version to 0.4.4
- Other bug fixes and improvements, see GitHub history

View File

@@ -0,0 +1,6 @@
- [Prowlarr Cardigann Definitions Schema Versions and Validations created](https://github.com/Prowlarr/indexers#schemas)
- [*Coming Soon* - Newznab & All Indexer Definitions to YML - Cardigann v8](https://github.com/Prowlarr/Prowlarr/pull/823)
- Note that users of Newznab (Usenet) Indexers may see that the UI shows Indexers as added that are not.
- This will be fixed with Cardigann v8 and is due to all the Newznab Indexers sharing the same definition.
- https://i.imgur.com/tijCHlk.png

View File

@@ -0,0 +1,6 @@
- **Users who do not wish to be on the alpha `nightly` testing branch should take advantage of this parity and switch to `develop`**
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.

View File

@@ -0,0 +1,6 @@
- **Users who do not wish to be on the alpha `nightly` or beta `develop` testing branches should take advantage of this parity and switch to `master`
A reminder about the `develop` and `nightly` branches
- **develop** - Current Develop/Beta - (Beta): This is the testing edge. Released after tested in nightly to ensure no immediate issues. New features and bug fixes released here first after nightly. It can be considered semi-stable, but is still beta.**
- **nightly** - Current Nightly/Unstable - (Alpha/Unstable) : This is the bleeding edge. It is released as soon as code is committed and passes all automated tests. This build may have not been used by us or other users yet. There is no guarantee that it will even run in some cases. This branch is only recommended for advanced users. Issues and self investigation are expected in this branch. Use this branch only if you know what you are doing and are willing to get your hands dirty to recover a failed update. This version is updated immediately.**

View File

@@ -0,0 +1,7 @@
- [Radarr Develop recently released](https://www.reddit.com/r/radarr/comments/w3kik4/new_release_develop_v4206438/)
- [Lidarr](https://lidarr.audio/donate), [Prowlarr](https://prowlarr.com/donate), [Radarr](https://radarr.video/donate), [Readarr](https://readarr.com/donate) now accept direct bitcoin donations
- Radarr Postgres Database Support in `nightly` and `develop`
- Prowlarr Postgres Database Support in `nightly` and `develop`
- Readarr Postgres Database Support in `nightly`
- [Lidarr Postgres Database Support in development (Draft PR#2625)](https://github.com/Lidarr/Lidarr/pull/2625)
- \*Arrs Wiki Contributions welcomed and strongly encouraged, simply auth with GitHub on the wiki and update the page

View File

@@ -7,7 +7,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions'; import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -48,7 +48,6 @@ const selectIsPopulated = createSelector(
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
(state) => state.settings.general.isPopulated, (state) => state.settings.general.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.appProfiles.isPopulated, (state) => state.settings.appProfiles.isPopulated,
(state) => state.indexers.isPopulated, (state) => state.indexers.isPopulated,
(state) => state.indexerStatus.isPopulated, (state) => state.indexerStatus.isPopulated,
@@ -59,7 +58,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
generalSettingsIsPopulated, generalSettingsIsPopulated,
languagesIsPopulated,
appProfilesIsPopulated, appProfilesIsPopulated,
indexersIsPopulated, indexersIsPopulated,
indexerStatusIsPopulated, indexerStatusIsPopulated,
@@ -71,7 +69,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
generalSettingsIsPopulated && generalSettingsIsPopulated &&
languagesIsPopulated &&
appProfilesIsPopulated && appProfilesIsPopulated &&
indexersIsPopulated && indexersIsPopulated &&
indexerStatusIsPopulated && indexerStatusIsPopulated &&
@@ -86,7 +83,6 @@ const selectErrors = createSelector(
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
(state) => state.settings.general.error, (state) => state.settings.general.error,
(state) => state.settings.languages.error,
(state) => state.settings.appProfiles.error, (state) => state.settings.appProfiles.error,
(state) => state.indexers.error, (state) => state.indexers.error,
(state) => state.indexerStatus.error, (state) => state.indexerStatus.error,
@@ -97,7 +93,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -109,7 +104,6 @@ const selectErrors = createSelector(
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
generalSettingsError || generalSettingsError ||
languagesError ||
appProfilesError || appProfilesError ||
indexersError || indexersError ||
indexerStatusError || indexerStatusError ||
@@ -123,7 +117,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -166,9 +159,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchTags() { dispatchFetchTags() {
dispatch(fetchTags()); dispatch(fetchTags());
}, },
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexers() { dispatchFetchIndexers() {
dispatch(fetchIndexers()); dispatch(fetchIndexers());
}, },
@@ -216,7 +206,6 @@ class PageConnector extends Component {
if (!this.props.isPopulated) { if (!this.props.isPopulated) {
this.props.dispatchFetchCustomFilters(); this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchAppProfiles(); this.props.dispatchFetchAppProfiles();
this.props.dispatchFetchIndexers(); this.props.dispatchFetchIndexers();
this.props.dispatchFetchIndexerStatus(); this.props.dispatchFetchIndexerStatus();
@@ -242,7 +231,6 @@ class PageConnector extends Component {
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchTags, dispatchFetchTags,
dispatchFetchLanguages,
dispatchFetchAppProfiles, dispatchFetchAppProfiles,
dispatchFetchIndexers, dispatchFetchIndexers,
dispatchFetchIndexerStatus, dispatchFetchIndexerStatus,
@@ -283,7 +271,6 @@ PageConnector.propTypes = {
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired, dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchAppProfiles: PropTypes.func.isRequired, dispatchFetchAppProfiles: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired, dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchFetchIndexerStatus: PropTypes.func.isRequired, dispatchFetchIndexerStatus: PropTypes.func.isRequired,

View File

@@ -20,9 +20,9 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.MOVIE_CONTINUING, iconName: icons.MOVIE_CONTINUING,
title: 'Indexers', title: translate('Indexers'),
to: '/', to: '/',
alias: '/movies', alias: '/indexers',
children: [ children: [
{ {
title: translate('Stats'), title: translate('Stats'),
@@ -33,13 +33,13 @@ const links = [
{ {
iconName: icons.SEARCH, iconName: icons.SEARCH,
title: 'Search', title: translate('Search'),
to: '/search' to: '/search'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: 'History', title: translate('History'),
to: '/history' to: '/history'
}, },

View File

@@ -35,21 +35,21 @@ class IndexerIndexFooter extends PureComponent {
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.enabled} /> <div className={styles.enabled} />
<div> <div>
Enabled {translate('Enabled')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.redirected} /> <div className={styles.redirected} />
<div> <div>
Enabled, Redirected {translate('EnabledRedirected')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.disabled} /> <div className={styles.disabled} />
<div> <div>
Disabled {translate('Disabled')}
</div> </div>
</div> </div>
@@ -60,7 +60,7 @@ class IndexerIndexFooter extends PureComponent {
)} )}
/> />
<div> <div>
Error {translate('Error')}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -62,8 +62,6 @@ class UISettings extends Component {
...otherProps ...otherProps
} = this.props; } = this.props;
const uiLanguages = languages.filter((item) => item.value !== 'Original');
const themeOptions = Object.keys(themes) const themeOptions = Object.keys(themes)
.map((theme) => ({ key: theme, value: titleCase(theme) })); .map((theme) => ({ key: theme, value: titleCase(theme) }));
@@ -172,7 +170,7 @@ class UISettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="uiLanguage" name="uiLanguage"
values={uiLanguages} values={languages}
helpText={translate('UILanguageHelpText')} helpText={translate('UILanguageHelpText')}
helpTextWarning={translate('UILanguageHelpTextWarning')} helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange} onChange={onInputChange}

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import UISettings from './UISettings'; import UISettings from './UISettings';
@@ -11,18 +12,19 @@ const SECTION = 'ui';
function createLanguagesSelector() { function createLanguagesSelector() {
return createSelector( return createSelector(
(state) => state.settings.languages, (state) => state.localization,
(languages) => { (localization) => {
const items = languages.items; console.log(localization);
const filterItems = ['Any', 'Unknown'];
const items = localization.items;
if (!items) { if (!items) {
return []; return [];
} }
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => { const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
return { return {
key: item.id, key: item.value,
value: item.name value: item.name
}; };
}); });
@@ -51,6 +53,7 @@ const mapDispatchToProps = {
setUISettingsValue, setUISettingsValue,
saveUISettings, saveUISettings,
fetchUISettings, fetchUISettings,
fetchLocalizationOptions,
clearPendingChanges clearPendingChanges
}; };
@@ -61,6 +64,7 @@ class UISettingsConnector extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchUISettings(); this.props.fetchUISettings();
this.props.fetchLocalizationOptions();
} }
componentWillUnmount() { componentWillUnmount() {
@@ -96,6 +100,7 @@ UISettingsConnector.propTypes = {
setUISettingsValue: PropTypes.func.isRequired, setUISettingsValue: PropTypes.func.isRequired,
saveUISettings: PropTypes.func.isRequired, saveUISettings: PropTypes.func.isRequired,
fetchUISettings: PropTypes.func.isRequired, fetchUISettings: PropTypes.func.isRequired,
fetchLocalizationOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };

View File

@@ -1,48 +0,0 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.languages';
//
// Actions Types
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
//
// Action Creators
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
},
//
// Reducers
reducers: {
}
};

View File

@@ -36,31 +36,31 @@ export const defaultState = {
}, },
{ {
name: 'indexer', name: 'indexer',
label: 'Indexer', label: translate('Indexer'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'query', name: 'query',
label: 'Query', label: translate('Query'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'parameters', name: 'parameters',
label: 'Parameters', label: translate('Parameters'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'grabTitle', name: 'grabTitle',
label: 'Grab Title', label: translate('Grab Title'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'categories', name: 'categories',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
@@ -72,13 +72,13 @@ export const defaultState = {
}, },
{ {
name: 'source', name: 'source',
label: 'Source', label: translate('Source'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'elapsedTime', name: 'elapsedTime',
label: 'Elapsed Time', label: translate('Elapsed Time'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },

View File

@@ -7,6 +7,7 @@ import * as indexers from './indexerActions';
import * as indexerIndex from './indexerIndexActions'; import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions'; import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions'; import * as indexerStatus from './indexerStatusActions';
import * as localization from './localizationActions';
import * as oAuth from './oAuthActions'; import * as oAuth from './oAuthActions';
import * as paths from './pathActions'; import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions'; import * as providerOptions from './providerOptionActions';
@@ -25,6 +26,7 @@ export default [
paths, paths,
providerOptions, providerOptions,
releases, releases,
localization,
indexers, indexers,
indexerIndex, indexerIndex,
indexerStats, indexerStats,

View File

@@ -36,7 +36,7 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'select', name: 'select',
columnLabel: 'Select', columnLabel: translate('Select'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false, isModifiable: false,
@@ -51,7 +51,7 @@ export const defaultState = {
}, },
{ {
name: 'sortName', name: 'sortName',
label: 'Indexer Name', label: translate('IndexerName'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
@@ -88,7 +88,7 @@ export const defaultState = {
}, },
{ {
name: 'capabilities', name: 'capabilities',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },

View File

@@ -0,0 +1,39 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'localization';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_LOCALIZATION_OPTIONS = 'localization/fetchLocalizationOptions';
//
// Action Creators
export const fetchLocalizationOptions = createThunk(FETCH_LOCALIZATION_OPTIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_LOCALIZATION_OPTIONS]: createFetchHandler(section, '/localization/options')
});
//
// Reducers
export const reducers = createHandleActions({}, defaultState, section);

View File

@@ -8,7 +8,6 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories'; import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies'; import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
import ui from './Settings/ui'; import ui from './Settings/ui';
@@ -16,7 +15,6 @@ export * from './Settings/downloadClients';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/indexerCategories'; export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies'; export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications'; export * from './Settings/notifications';
export * from './Settings/applications'; export * from './Settings/applications';
export * from './Settings/appProfiles'; export * from './Settings/appProfiles';
@@ -38,7 +36,6 @@ export const defaultState = {
general: general.defaultState, general: general.defaultState,
indexerCategories: indexerCategories.defaultState, indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState, indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState, notifications: notifications.defaultState,
applications: applications.defaultState, applications: applications.defaultState,
appProfiles: appProfiles.defaultState, appProfiles: appProfiles.defaultState,
@@ -68,7 +65,6 @@ export const actionHandlers = handleThunks({
...general.actionHandlers, ...general.actionHandlers,
...indexerCategories.actionHandlers, ...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers, ...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers, ...notifications.actionHandlers,
...applications.actionHandlers, ...applications.actionHandlers,
...appProfiles.actionHandlers, ...appProfiles.actionHandlers,
@@ -89,7 +85,6 @@ export const reducers = createHandleActions({
...general.reducers, ...general.reducers,
...indexerCategories.reducers, ...indexerCategories.reducers,
...indexerProxies.reducers, ...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers, ...notifications.reducers,
...applications.reducers, ...applications.reducers,
...appProfiles.reducers, ...appProfiles.reducers,

View File

@@ -15,27 +15,27 @@ const columns = [
}, },
{ {
name: 'commandName', name: 'commandName',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'queued', name: 'queued',
label: 'Queued', label: translate('Queued'),
isVisible: true isVisible: true
}, },
{ {
name: 'started', name: 'started',
label: 'Started', label: translate('Started'),
isVisible: true isVisible: true
}, },
{ {
name: 'ended', name: 'ended',
label: 'Ended', label: translate('Ended'),
isVisible: true isVisible: true
}, },
{ {
name: 'duration', name: 'duration',
label: 'Duration', label: translate('Duration'),
isVisible: true isVisible: true
}, },
{ {

View File

@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [ const columns = [
{ {
name: 'name', name: 'name',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'interval', name: 'interval',
label: 'Interval', label: translate('Interval'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastExecution', name: 'lastExecution',
label: 'Last Execution', label: translate('LastExecution'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastDuration', name: 'lastDuration',
label: 'Last Duration', label: translate('LastDuration'),
isVisible: true isVisible: true
}, },
{ {
name: 'nextExecution', name: 'nextExecution',
label: 'Next Execution', label: translate('NextExecution'),
isVisible: true isVisible: true
}, },
{ {

View File

@@ -0,0 +1,105 @@
#!/bin/bash
# Generate a Markdown change log of pull requests from commits between two tags
scriptDir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
ghRepo="Prowlarr"
#branch="develop"
#read -r -p "What Repo?: " ghRepo
#read -r -p "What Org?: [Default:$ghRepo]" ghOrg
read -r -p "What Branch? [master|develop|nightly]:" branch
ghOrg=${ghOrg:-$ghRepo}
ghRepoUrl=https://github.com/$ghOrg/$ghRepo
case "${branch}" in
master)
hotioBranch='release'
lsioBranch='latest'
branchType='Stable'
;;
develop)
hotioBranch='testing'
lsioBranch='develop'
branchType='Beta'
;;
nightly)
hotioBranch='nightly'
lsioBranch='nightly'
branchType='Alpha'
;;
esac
baseDir=$(dirname "$scriptDir")
changelogDir="$baseDir/changelogs/"
templateDir="$changelogDir/templates/"
# Get a list of all tags in reverse order
# Assumes the tags are in version format like v1.2.3
gitTags=$(git ls-remote -t --exit-code --refs --sort='-v:refname' "$ghRepoUrl" | sed -E 's/^[[:xdigit:]]+[[:space:]]+refs\/tags\/(.+)/\1/g')
# Make the tags an array
# shellcheck disable=SC2206
tags=($gitTags)
latestTag=${tags[0]}
previousTag=${tags[1]}
# Get a log of commits that occurred between two tags
# See Pretty format placeholders at https://git-scm.com/docs/pretty-formats
# -i -E --grep="(Fixed:|New:)"'
commits=$(git log --pretty=format:' - %s%n' "$previousTag".."$latestTag")
# Store our changelog in a variable to be saved to a file at the end
markdown="# New ${branchType^} Release"
markdown+='\n\n'
markdown+="$ghRepo $latestTag has been released on \`$branch\`"
markdown+='\n\n'
branchmsg=$(cat "$templateDir"/branch-$branch.md)
if [ -n "$branchmsg" ]; then
{
markdown+=$branchmsg
markdown+='\n\n'
}
fi
markdown+="# Announcements"
markdown+='\n\n'
markdown+=$(cat "$templateDir"/announcements.md)
markdown+='\n\n'
markdown+="# Additional Commentary"
markdown+='\n\n'
markdown+=$(cat "$templateDir"/commentary.md)
markdown+='\n\n'
markdown+="# Releases"
markdown+='\n\n'
markdown+="## Native"
markdown+="\n\n"
markdown+="- [GitHub Releases]($ghRepoUrl/releases)"
markdown+="\n\n"
markdown+="- [Wiki Installation Instructions](https://wiki.servarr.com/${ghRepo,,}/installation)"
markdown+="\n\n"
markdown+="## Docker"
markdown+="\n\n"
markdown+="- [hotio/$ghRepo:$hotioBranch](https://hotio.dev/containers/${ghRepo,,})"
markdown+="\n\n"
markdown+="- [lscr.io/linuxserver/$ghRepo:$lsioBranch](https://docs.linuxserver.io/images/docker-${ghRepo,,})"
markdown+="\n\n"
markdown+="## NAS Packages"
markdown+="\n\n"
markdown+="- Synology - Please ask the SynoCommunity to update the base package; however, you can update in-app normally"
markdown+="\n\n"
markdown+="- QNAP - Please ask the QNAP to update the base package; however, you should be able to update in-app normally"
markdown+="\n\n"
markdown+="------------"
markdown+="\n\n"
markdown+="# Release Notes"
markdown+="\n\n"
markdown+="## $latestTag (changes since $previousTag)"
markdown+="\n\n"
markdown+="$commits"
markdown+="\n\n"
markdown+=" - Other bug fixes and improvements, see GitHub history"
# Loop over each commit and look for merged pull requests
#for COMMIT in $COMMITS; do
#done
# Save our markdown to a file
mkdir -p "$changelogDir"
echo -e "$markdown" >"$changelogDir/CHANGELOG-$latestTag.md"
exit 0

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -15,8 +16,11 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories; using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http namespace NzbDrone.Common.Test.Http
{ {
@@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost; private string _httpBinHost;
private string _httpBinHost2; private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp] [OneTimeSetUp]
public void FixtureSetUp() public void FixtureSetUp()
{ {
@@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com"; var mainHost = "httpbin.servarr.com";
// Use mirrors for tests that use two hosts // Use mirrors for tests that use two hosts
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" }; var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable. // httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHost = mainHost; _httpBinHost = mainHost;
@@ -46,31 +52,22 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available."); TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10; _httpBinSleep = 10;
} }
private bool IsTestSiteAvailable(string site) private bool IsTestSiteAvailable(string site)
{ {
try try
{ {
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest; var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK) if (res.StatusCode != HttpStatusCode.OK)
{ {
return false; return false;
} }
try res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429) if (res == null || res.StatusCode != HttpStatusCode.TooManyRequests)
{ {
return false; return false;
} }
@@ -95,10 +92,13 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0"); Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>()); Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>()); Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>()); Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>()); Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>()); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>()); Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[TestCase(CertificateValidationType.Enabled)]
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test] [Test]
public void should_execute_typed_get() public void should_execute_typed_get()
{ {
@@ -162,15 +184,44 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[TestCase("gzip")] [Test]
public void should_execute_get_using_gzip(string compression) public void should_execute_post_with_content_type()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/{compression}"); var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
response.Resource.Gzipped.Should().BeTrue(); response.Resource.Gzipped.Should().BeTrue();
response.Resource.Brotli.Should().BeFalse();
}
[Test]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
response.Resource.Gzipped.Should().BeFalse();
response.Resource.Brotli.Should().BeTrue();
} }
[TestCase(HttpStatusCode.Unauthorized)] [TestCase(HttpStatusCode.Unauthorized)]
@@ -190,6 +241,28 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test] [Test]
public void should_not_follow_redirects_when_not_in_production() public void should_not_follow_redirects_when_not_in_production()
{ {
@@ -315,13 +388,38 @@ namespace NzbDrone.Common.Test.Http
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file)); Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse(); File.Exists(file).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
[Test]
public void should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
using (var fileStream = new FileStream(file, FileMode.Create))
{
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
ExceptionVerification.ExpectedErrors(1);
File.Exists(file).Should().BeTrue();
var fileInfo = new FileInfo(file);
fileInfo.Length.Should().Be(0);
}
[Test] [Test]
public void should_send_cookie() public void should_send_cookie()
{ {
@@ -753,6 +851,7 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; } public string Url { get; set; }
public string Data { get; set; } public string Data { get; set; }
public bool Gzipped { get; set; } public bool Gzipped { get; set; }
public bool Brotli { get; set; }
} }
public class HttpCookieResource public class HttpCookieResource

View File

@@ -210,5 +210,26 @@ namespace NzbDrone.Common.Extensions
return result.TrimStart(' ', '.').TrimEnd(' '); return result.TrimStart(' ', '.').TrimEnd(' ');
} }
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
} }
} }

View File

@@ -0,0 +1,12 @@
using System.Net;
namespace NzbDrone.Common.Http
{
public class BasicNetworkCredential : NetworkCredential
{
public BasicNetworkCredential(string user, string pass)
: base(user, pass)
{
}
}
}

View File

@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Http
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should // NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)"); private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' }; private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' }; private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -24,7 +25,7 @@ namespace NzbDrone.Common.Http
var matches = _CookieRegex.Match(cookieHeader); var matches = _CookieRegex.Match(cookieHeader);
while (matches.Success) while (matches.Success)
{ {
if (matches.Groups.Count > 2) if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
{ {
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value; cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
} }

View File

@@ -0,0 +1,11 @@
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface ICertificateValidationService
{
bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
}
}

View File

@@ -6,6 +6,5 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher public interface IHttpDispatcher
{ {
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies); Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
Task DownloadFileAsync(string url, string fileName);
} }
} }

View File

@@ -1,13 +1,16 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NLog.Fluent; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
@@ -15,221 +18,224 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
public class ManagedHttpDispatcher : IHttpDispatcher public class ManagedHttpDispatcher : IHttpDispatcher
{ {
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy; private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder; private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo; private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly Logger _logger; private readonly ICached<CredentialCache> _credentialCache;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
{ {
_proxySettingsProvider = proxySettingsProvider; _proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy; _createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_userAgentBuilder = userAgentBuilder; _userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger; _httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
} }
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies) public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{ {
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
// Deflate is not a standard and could break depending on implementation. var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
// we should just stick with the more compatible Gzip if (cookieHeader.IsNotNullOrWhiteSpace())
//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);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{ {
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); requestMessage.Headers.Add("Cookie", cookieHeader);
} }
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url); using var cts = new CancellationTokenSource();
if (request.RequestTimeout != TimeSpan.Zero)
{
cts.CancelAfter(request.RequestTimeout);
}
else
{
// The default for System.Net.Http.HttpClient
cts.CancelAfter(TimeSpan.FromSeconds(100));
}
if (request.Credentials != null)
{
if (request.Credentials is BasicNetworkCredential bc)
{
// Manually set header to avoid initial challenge response
var authInfo = bc.UserName + ":" + bc.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
}
else if (request.Credentials is NetworkCredential nc)
{
var creds = GetCredentialCache();
foreach (var authtype in new[] { "Basic", "Digest" })
{
creds.Remove((Uri)request.Url, authtype);
creds.Add((Uri)request.Url, authtype, nc);
}
}
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
if (request.Headers != null) if (request.Headers != null)
{ {
AddRequestHeaders(webRequest, request.Headers); AddRequestHeaders(requestMessage, request.Headers);
} }
HttpWebResponse httpWebResponse; var httpClient = GetClient(request.Url);
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
try using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{ {
if (request.ContentData != null) byte[] data = null;
{
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
httpWebResponse = (HttpWebResponse)await webRequest.GetResponseAsync(); try
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{ {
// The default messages for WebException on mono are pretty horrible. if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{ {
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
}
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
} }
else else
{ {
throw; data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
} }
} }
} catch (Exception ex)
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
{ {
try throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
{ {
data = await responseStream.ToBytes(); try
} {
catch (Exception ex) cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
{ }
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); catch
{
// Ignore invalid cookies
}
} }
} }
}
sw.Stop(); var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, sw.ElapsedMilliseconds, httpWebResponse.StatusCode); sw.Stop();
}
public async Task DownloadFileAsync(string url, string fileName) return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
{
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
var uri = new HttpUri(url);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
await webClient.DownloadFileTaskAsync(url, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
}
catch (WebException e)
{
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
} }
} }
protected virtual IWebProxy GetProxy(HttpUri uri) protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri)
{ {
IWebProxy proxy = null;
var proxySettings = _proxySettingsProvider.GetProxySettings(uri); var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
var key = proxySettings?.Key ?? NO_PROXY_KEY;
return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings));
}
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
{
var handler = new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
UseCookies = false, // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false,
Credentials = GetCredentialCache(),
PreAuthenticate = true,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
}
};
if (proxySettings != null) if (proxySettings != null)
{ {
proxy = _createManagedWebProxy.GetWebProxy(proxySettings); handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
} }
return proxy; var client = new System.Net.Http.HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
} }
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers) protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
{ {
foreach (var header in headers) foreach (var header in headers)
{ {
switch (header.Key) switch (header.Key)
{ {
case "Accept": case "Accept":
webRequest.Accept = header.Value; webRequest.Headers.Accept.ParseAdd(header.Value);
break; break;
case "Connection": case "Connection":
webRequest.Connection = header.Value; webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
break; break;
case "Content-Length": case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value); AddContentHeader(webRequest, "Content-Length", header.Value);
break; break;
case "Content-Type": case "Content-Type":
webRequest.ContentType = header.Value; AddContentHeader(webRequest, "Content-Type", header.Value);
break; break;
case "Date": case "Date":
webRequest.Date = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Expect": case "Expect":
webRequest.Expect = header.Value; webRequest.Headers.Expect.ParseAdd(header.Value);
break; break;
case "Host": case "Host":
webRequest.Host = header.Value; webRequest.Headers.Host = header.Value;
break; break;
case "If-Modified-Since": case "If-Modified-Since":
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Range": case "Range":
throw new NotImplementedException(); throw new NotImplementedException();
case "Referer": case "Referer":
webRequest.Referer = header.Value; webRequest.Headers.Add("Referer", header.Value);
break; break;
case "Transfer-Encoding": case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value; webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
break; break;
case "User-Agent": case "User-Agent":
webRequest.UserAgent = header.Value; webRequest.Headers.UserAgent.Clear();
webRequest.Headers.UserAgent.ParseAdd(header.Value);
break; break;
case "Proxy-Connection": case "Proxy-Connection":
throw new NotImplementedException(); throw new NotImplementedException();
@@ -239,5 +245,84 @@ namespace NzbDrone.Common.Http.Dispatchers
} }
} }
} }
private void AddContentHeader(HttpRequestMessage request, string header, string value)
{
var headers = request.Content?.Headers;
if (headers == null)
{
return;
}
headers.Remove(header);
headers.Add(header, value);
}
private CredentialCache GetCredentialCache()
{
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
if (useIPv6)
{
try
{
var localToken = cancellationToken;
if (!hasResolvedIPv6Availability)
{
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
localToken = linkedTokenSource.Token;
}
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false;
}
finally
{
hasResolvedIPv6Availability = true;
}
}
// fallback to IPv4.
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
}
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
} }
} }

View File

@@ -1,15 +0,0 @@
using System;
using System.Net;
namespace NzbDrone.Common.Http
{
public class GZipWebClient : WebClient
{
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
request.AutomaticDecompression = DecompressionMethods.GZip;
return request;
}
}
}

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -86,13 +87,21 @@ namespace NzbDrone.Common.Http
} }
// 302 or 303 should default to GET on redirect even if POST on original // 302 or 303 should default to GET on redirect even if POST on original
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod) if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
{ {
request.Method = HttpMethod.Get; request.Method = HttpMethod.Get;
request.ContentData = null; request.ContentData = null;
} }
response = await ExecuteRequestAsync(request, cookieContainer); // Save to add to final response
var responseCookies = response.Cookies;
// Update cookiecontainer for next request with any cookies recieved on last request
var responseContainer = HandleRedirectCookies(request, response);
response = await ExecuteRequestAsync(request, responseContainer);
response.Cookies.Add(responseCookies);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
} }
@@ -102,11 +111,14 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
} }
if (!request.SuppressHttpError && response.HasHttpError) if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
{ {
_logger.Warn("HTTP Error - {0}", response); if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
if ((int)response.StatusCode == 429) if (response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(request, response); throw new TooManyRequestsException(request, response);
} }
@@ -124,6 +136,21 @@ namespace NzbDrone.Common.Http
return ExecuteAsync(request).GetAwaiter().GetResult(); return ExecuteAsync(request).GetAwaiter().GetResult();
} }
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
switch (statusCode)
{
case HttpStatusCode.Moved:
case HttpStatusCode.Found:
case HttpStatusCode.MultipleChoices:
return requestMethod == HttpMethod.Post;
case HttpStatusCode.SeeOther:
return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
default:
return false;
}
}
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer) private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)
@@ -140,8 +167,6 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer); var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer); HandleResponseCookies(response, cookieContainer);
@@ -208,52 +233,125 @@ namespace NzbDrone.Common.Http
} }
} }
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer) private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
{ {
// Don't collect persistnet cookies for intermediate/redirected urls. var sourceContainer = new CookieContainer();
/*lock (_cookieContainerCache) var responseCookies = response.GetCookies();
if (responseCookies.Count != 0)
{ {
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); foreach (var pair in responseCookies)
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); {
var existingCookies = cookieContainer.GetCookies((Uri)request.Url); 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)
};
}
cookieContainer.Add(persistentCookies); sourceContainer.Add((Uri)request.Url, cookie);
cookieContainer.Add(existingCookies); }
}*/ }
return sourceContainer;
} }
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer) private void HandleResponseCookies(HttpResponse response, CookieContainer container)
{ {
foreach (Cookie cookie in container.GetAllCookies())
{
cookie.Expired = true;
}
var cookieHeaders = response.GetCookieHeaders(); var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty()) if (cookieHeaders.Empty())
{ {
return; return;
} }
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie) if (response.Request.StoreResponseCookie)
{ {
lock (_cookieContainerCache) lock (_cookieContainerCache)
{ {
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
foreach (var cookieHeader in cookieHeaders) AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
{ }
try }
{ }
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
} private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
catch (Exception ex) {
{ foreach (var cookieHeader in cookieHeaders)
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url); {
} try
} {
container.SetCookies((Uri)url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", url);
} }
} }
} }
public async Task DownloadFileAsync(string url, string fileName) public async Task DownloadFileAsync(string url, string fileName)
{ {
await _httpDispatcher.DownloadFileAsync(url, fileName); var fileNamePart = fileName + ".part";
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new HttpException(request, response, "Site responded with html content.");
}
}
stopWatch.Stop();
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.Move(fileNamePart, fileName);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
finally
{
if (File.Exists(fileNamePart))
{
File.Delete(fileNamePart);
}
}
} }
public void DownloadFile(string url, string fileName) public void DownloadFile(string url, string fileName)

View File

@@ -4,11 +4,27 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public static class WebHeaderCollectionExtensions
{
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
{
var result = new NameValueCollection();
foreach (var header in headers)
{
result.Add(header.Key, header.Value.ConcatToString(";"));
}
return result;
}
}
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
{ {
public HttpHeader(NameValueCollection headers) public HttpHeader(NameValueCollection headers)
@@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http
{ {
} }
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
public HttpHeader() public HttpHeader()
{ {
} }
@@ -107,6 +128,30 @@ namespace NzbDrone.Common.Http
} }
} }
public string ContentEncoding
{
get
{
return GetSingleValue("Content-Encoding");
}
set
{
SetSingleValue("Content-Encoding", value);
}
}
public string Vary
{
get
{
return GetSingleValue("Vary");
}
set
{
SetSingleValue("Vary", value);
}
}
public string UserAgent public string UserAgent
{ {
get get

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@@ -12,11 +13,13 @@ namespace NzbDrone.Common.Http
{ {
public HttpRequest(string url, HttpAccept httpAccept = null) public HttpRequest(string url, HttpAccept httpAccept = null)
{ {
Method = HttpMethod.Get;
Url = new HttpUri(url); Url = new HttpUri(url);
Headers = new HttpHeader(); Headers = new HttpHeader();
Method = HttpMethod.Get; Method = HttpMethod.Get;
ConnectionKeepAlive = true; ConnectionKeepAlive = true;
AllowAutoRedirect = true; AllowAutoRedirect = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction) if (!RuntimeInfo.IsProduction)
@@ -37,16 +40,20 @@ namespace NzbDrone.Common.Http
public IWebProxy Proxy { get; set; } public IWebProxy Proxy { get; set; }
public byte[] ContentData { get; set; } public byte[] ContentData { get; set; }
public string ContentSummary { get; set; } public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; } public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString() public override string ToString()
{ {
@@ -103,12 +110,5 @@ namespace NzbDrone.Common.Http
return encoding.GetString(ContentData); return encoding.GetString(ContentData);
} }
} }
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
Headers.Set("Authorization", "Basic " + authInfo);
}
} }
} }

View File

@@ -21,12 +21,13 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; } public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; } public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public NetworkCredential NetworkCredential { get; set; } public ICredentials NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
@@ -46,6 +47,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader(); Headers = new HttpHeader();
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>(); FormData = new List<HttpFormData>();
LogHttpError = true;
} }
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null) public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -106,6 +108,7 @@ namespace NzbDrone.Common.Http
request.Method = Method; request.Method = Method;
request.Encoding = Encoding; request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError; request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent; request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect; request.AllowAutoRedirect = AllowAutoRedirect;
request.StoreRequestCookie = StoreRequestCookie; request.StoreRequestCookie = StoreRequestCookie;
@@ -113,13 +116,7 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive; request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit; request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent; request.LogResponseContent = LogResponseContent;
request.Credentials = NetworkCredential;
if (NetworkCredential != null)
{
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers.Set("Authorization", "Basic " + authInfo);
}
foreach (var header in Headers) foreach (var header in Headers)
{ {
@@ -212,7 +209,7 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.UTF8.GetString(formData.ContentData)); summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.GetString(formData.ContentData));
} }
} }
@@ -232,9 +229,9 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.UTF8.GetString(v.ContentData)))); var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.GetString(v.ContentData))));
var urlencoded = string.Join("&", parameters); var urlencoded = string.Join("&", parameters);
var body = Encoding.UTF8.GetBytes(urlencoded); var body = Encoding.GetBytes(urlencoded);
request.Headers.ContentType = "application/x-www-form-urlencoded"; request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(body); request.SetContent(body);
@@ -406,7 +403,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData FormData.Add(new HttpFormData
{ {
Name = key, Name = key,
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)) ContentData = Encoding.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
}); });
return this; return this;

View File

@@ -64,10 +64,11 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400; public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved || public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found || StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.MultipleChoices ||
StatusCode == HttpStatusCode.PermanentRedirect ||
Headers.ContainsKey("Refresh"); Headers.ContainsKey("Refresh");
public string RedirectUrl public string RedirectUrl
@@ -117,7 +118,7 @@ namespace NzbDrone.Common.Http
public override string ToString() public override string ToString()
{ {
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode); var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{ {

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http
{
public class XmlRpcRequestBuilder : HttpRequestBuilder
{
public static string XmlRpcContentType = "text/xml";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
public string XmlMethod { get; private set; }
public List<object> XmlParameters { get; private set; }
public XmlRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
XmlParameters = new List<object>();
}
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
: this(BuildBaseUrl(useHttps, host, port, urlBase))
{
}
public override HttpRequestBuilder Clone()
{
var clone = base.Clone() as XmlRpcRequestBuilder;
clone.XmlParameters = new List<object>(XmlParameters);
return clone;
}
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
{
var clone = Clone() as XmlRpcRequestBuilder;
clone.XmlMethod = method;
clone.XmlParameters = parameters.ToList();
return clone;
}
protected override void Apply(HttpRequest request)
{
base.Apply(request);
request.Headers.ContentType = XmlRpcContentType;
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
if (XmlParameters.Any())
{
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
var paramsElement = new XElement("params", argElements);
methodCallElements.Add(paramsElement);
}
var message = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("methodCall", methodCallElements));
var body = message.ToString();
Logger.Debug($"Executing remote method: {XmlMethod}");
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
request.SetContent(body);
}
private static XElement ConvertParameter(object value)
{
XElement data;
if (value is string s)
{
data = new XElement("string", s);
}
else if (value is List<string> l)
{
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
}
else if (value is int i)
{
data = new XElement("int", i);
}
else if (value is byte[] bytes)
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
}
return new XElement("value", data);
}
}
}

View File

@@ -6,12 +6,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" /> <PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.18.0" /> <PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http; using NzbDrone.Core.Http;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
@@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>())); Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger)); Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder()); Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
} }

View File

@@ -60,6 +60,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.ImdbId.Should().Be(15569106); torrentInfo.ImdbId.Should().Be(15569106);
torrentInfo.TmdbId.Should().Be(135144); torrentInfo.TmdbId.Should().Be(135144);
torrentInfo.TvdbId.Should().Be(410548); torrentInfo.TvdbId.Should().Be(410548);
torrentInfo.Languages.Should().HaveCount(1);
torrentInfo.Languages.First().Should().Be("Japanese");
torrentInfo.Subs.Should().HaveCount(27);
torrentInfo.Subs.First().Should().Be("Arabic");
} }
} }
} }

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
{
[TestFixture]
public class GazelleGamesFixture : CoreTest<GazelleGames>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "GazelleGames",
Settings = new GazelleGamesSettings() { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_GazelleGames()
{
var recentFeed = ReadAllText(@"Files/Indexers/GazelleGames/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(1464);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://gazellegames.net/torrents.php?action=download&id=303216&authkey=prowlarr&torrent_pass=");
torrentInfo.InfoUrl.Should().Be("https://gazellegames.net/torrents.php?id=84781&torrentid=303216");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime());
torrentInfo.Size.Should().Be(80077617780);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(383);
torrentInfo.Seeders.Should().Be(383);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -1,85 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Languages
{
[TestFixture]
public class LanguageFixture : CoreTest
{
public static object[] FromIntCases =
{
new object[] { 1, Language.English },
new object[] { 2, Language.French },
new object[] { 3, Language.Spanish },
new object[] { 4, Language.German },
new object[] { 5, Language.Italian },
new object[] { 6, Language.Danish },
new object[] { 7, Language.Dutch },
new object[] { 8, Language.Japanese },
new object[] { 9, Language.Icelandic },
new object[] { 10, Language.Chinese },
new object[] { 11, Language.Russian },
new object[] { 12, Language.Polish },
new object[] { 13, Language.Vietnamese },
new object[] { 14, Language.Swedish },
new object[] { 15, Language.Norwegian },
new object[] { 16, Language.Finnish },
new object[] { 17, Language.Turkish },
new object[] { 18, Language.Portuguese },
new object[] { 19, Language.Flemish },
new object[] { 20, Language.Greek },
new object[] { 21, Language.Korean },
new object[] { 22, Language.Hungarian },
new object[] { 23, Language.Hebrew },
new object[] { 24, Language.Lithuanian },
new object[] { 25, Language.Czech }
};
public static object[] ToIntCases =
{
new object[] { Language.English, 1 },
new object[] { Language.French, 2 },
new object[] { Language.Spanish, 3 },
new object[] { Language.German, 4 },
new object[] { Language.Italian, 5 },
new object[] { Language.Danish, 6 },
new object[] { Language.Dutch, 7 },
new object[] { Language.Japanese, 8 },
new object[] { Language.Icelandic, 9 },
new object[] { Language.Chinese, 10 },
new object[] { Language.Russian, 11 },
new object[] { Language.Polish, 12 },
new object[] { Language.Vietnamese, 13 },
new object[] { Language.Swedish, 14 },
new object[] { Language.Norwegian, 15 },
new object[] { Language.Finnish, 16 },
new object[] { Language.Turkish, 17 },
new object[] { Language.Portuguese, 18 },
new object[] { Language.Flemish, 19 },
new object[] { Language.Greek, 20 },
new object[] { Language.Korean, 21 },
new object[] { Language.Hungarian, 22 },
new object[] { Language.Hebrew, 23 },
new object[] { Language.Lithuanian, 24 },
new object[] { Language.Czech, 25 }
};
[Test]
[TestCaseSource("FromIntCases")]
public void should_be_able_to_convert_int_to_languageTypes(int source, Language expected)
{
var language = (Language)source;
language.Should().Be(expected);
}
[Test]
[TestCaseSource("ToIntCases")]
public void should_be_able_to_convert_languageTypes_to_int(Language source, int expected)
{
var i = (int)source;
i.Should().Be(expected);
}
}
}

View File

@@ -3,7 +3,6 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -16,7 +15,7 @@ namespace NzbDrone.Core.Test.Localization
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English); Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("en");
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory); Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
} }

View File

@@ -6,7 +6,6 @@ using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
@@ -139,9 +138,9 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableColorImpairedMode", value); } set { SetValue("EnableColorImpairedMode", value); }
} }
public int UILanguage public string UILanguage
{ {
get { return GetValueInt("UILanguage", (int)Language.English); } get { return GetValue("UILanguage", "en"); }
set { SetValue("UILanguage", value); } set { SetValue("UILanguage", value); }
} }

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Configuration
string TimeFormat { get; set; } string TimeFormat { get; set; }
bool ShowRelativeDates { get; set; } bool ShowRelativeDates { get; set; }
bool EnableColorImpairedMode { get; set; } bool EnableColorImpairedMode { get; set; }
int UILanguage { get; set; } string UILanguage { get; set; }
//Internal //Internal
string PlexClientIdentifier { get; } string PlexClientIdentifier { get; }

View File

@@ -1,48 +0,0 @@
using System;
using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapper;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Datastore.Converters
{
public class DapperLanguageIntConverter : SqlMapper.TypeHandler<Language>
{
public override void SetValue(IDbDataParameter parameter, Language value)
{
if (value == null)
{
throw new InvalidOperationException("Attempted to save a language that isn't really a language");
}
else
{
parameter.Value = (int)value;
}
}
public override Language Parse(object value)
{
if (value == null || value is DBNull)
{
return Language.Unknown;
}
return (Language)Convert.ToInt32(value);
}
}
public class LanguageIntConverter : JsonConverter<Language>
{
public override Language Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var item = reader.GetInt32();
return (Language)item;
}
public override void Write(Utf8JsonWriter writer, Language value, JsonSerializerOptions options)
{
writer.WriteNumberValue((int)value);
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(021)]
public class localization_setting_to_string : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(FixLocalizationConfig);
}
private void FixLocalizationConfig(IDbConnection conn, IDbTransaction tran)
{
string uiLanguage;
string uiCulture;
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'uilanguage'";
uiLanguage = (string)cmd.ExecuteScalar();
}
if (uiLanguage != null && int.TryParse(uiLanguage, out var uiLanguageInt))
{
uiCulture = _uiMapping.GetValueOrDefault(uiLanguageInt) ?? "en";
using (var insertCmd = conn.CreateCommand())
{
insertCmd.Transaction = tran;
insertCmd.CommandText = string.Format("UPDATE \"Config\" SET \"Value\" = '{0}' WHERE \"Key\" = 'uilanguage'", uiCulture);
insertCmd.ExecuteNonQuery();
}
}
}
private readonly Dictionary<int, string> _uiMapping = new Dictionary<int, string>()
{
{ 1, "en" },
{ 2, "fr" },
{ 3, "es" },
{ 4, "de" },
{ 5, "it" },
{ 6, "da" },
{ 7, "nl" },
{ 8, "ja" },
{ 9, "is" },
{ 10, "zh_CN" },
{ 11, "ru" },
{ 12, "pl" },
{ 13, "vi" },
{ 14, "sv" },
{ 15, "nb_NO" },
{ 16, "fi" },
{ 17, "tr" },
{ 18, "pt" },
{ 19, "en" },
{ 20, "el" },
{ 21, "ko" },
{ 22, "hu" },
{ 23, "he" },
{ 24, "lt" },
{ 25, "cs" },
{ 26, "hi" },
{ 27, "ro" },
{ 28, "th" },
{ 29, "bg" },
{ 30, "pt_BR" },
{ 31, "ar" },
{ 32, "uk" },
{ 33, "fa" },
{ 34, "be" },
{ 35, "zh_TW" },
};
}
}

View File

@@ -14,7 +14,6 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerVersions; using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs; using NzbDrone.Core.Jobs;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -113,8 +112,6 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());
SqlMapper.AddTypeHandler(new DapperLanguageIntConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter()));
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());

View File

@@ -1,111 +1,161 @@
using CookComputing.XmlRpc; using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2 namespace NzbDrone.Core.Download.Clients.Aria2
{ {
public class Aria2Version public class Aria2Fault
{ {
[XmlRpcMember("version")] public Aria2Fault(XElement element)
public string Version; {
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").ElementAsInt("int");
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("enabledFeatures")] public int FaultCode { get; set; }
public string[] EnabledFeatures; public string FaultString { get; set; }
} }
public class Aria2Uri public class Aria2Version
{ {
[XmlRpcMember("status")] public Aria2Version(XElement element)
public string Status; {
foreach (var e in element.XPathSelectElements("./struct/member"))
{
if (e.ElementAsString("name") == "version")
{
Version = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("uri")] public string Version { get; set; }
public string Uri;
} }
public class Aria2File public class Aria2File
{ {
[XmlRpcMember("index")] public Aria2File(XElement element)
public string Index; {
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("length")] if (name == "path")
public string Length; {
Path = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("completedLength")] public string Path { get; set; }
public string CompletedLength; }
[XmlRpcMember("path")] public class Aria2Dict
public string Path; {
public Aria2Dict(XElement element)
{
Dict = new Dictionary<string, string>();
[XmlRpcMember("selected")] foreach (var e in element.XPathSelectElements("./struct/member"))
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public string Selected; Dict.Add(e.ElementAsString("name"), e.Element("value").GetStringValue());
}
}
[XmlRpcMember("uris")] public Dictionary<string, string> Dict { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] }
public Aria2Uri[] Uris;
public class Aria2Bittorrent
{
public Aria2Bittorrent(XElement element)
{
foreach (var e in element.Descendants("member"))
{
if (e.ElementAsString("name") == "name")
{
Name = e.Element("value").GetStringValue();
}
}
}
public string Name;
} }
public class Aria2Status public class Aria2Status
{ {
[XmlRpcMember("bittorrent")] public Aria2Status(XElement element)
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public XmlRpcStruct Bittorrent; foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("bitfield")] if (name == "bittorrent")
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public string Bitfield; Bittorrent = new Aria2Bittorrent(e.Element("value"));
}
else if (name == "infoHash")
{
InfoHash = e.Element("value").GetStringValue();
}
else if (name == "completedLength")
{
CompletedLength = e.Element("value").GetStringValue();
}
else if (name == "downloadSpeed")
{
DownloadSpeed = e.Element("value").GetStringValue();
}
else if (name == "files")
{
Files = e.XPathSelectElement("./value/array/data")
.Elements()
.Select(x => new Aria2File(x))
.ToArray();
}
else if (name == "gid")
{
Gid = e.Element("value").GetStringValue();
}
else if (name == "status")
{
Status = e.Element("value").GetStringValue();
}
else if (name == "totalLength")
{
TotalLength = e.Element("value").GetStringValue();
}
else if (name == "uploadLength")
{
UploadLength = e.Element("value").GetStringValue();
}
else if (name == "errorMessage")
{
ErrorMessage = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("infoHash")] public Aria2Bittorrent Bittorrent { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string InfoHash { get; set; }
public string InfoHash; public string CompletedLength { get; set; }
public string DownloadSpeed { get; set; }
[XmlRpcMember("completedLength")] public Aria2File[] Files { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string Gid { get; set; }
public string CompletedLength; public string Status { get; set; }
public string TotalLength { get; set; }
[XmlRpcMember("connections")] public string UploadLength { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string ErrorMessage { get; set; }
public string Connections;
[XmlRpcMember("dir")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Dir;
[XmlRpcMember("downloadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string DownloadSpeed;
[XmlRpcMember("files")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2File[] Files;
[XmlRpcMember("gid")]
public string Gid;
[XmlRpcMember("numPieces")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string NumPieces;
[XmlRpcMember("pieceLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string PieceLength;
[XmlRpcMember("status")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Status;
[XmlRpcMember("totalLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string TotalLength;
[XmlRpcMember("uploadLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadLength;
[XmlRpcMember("uploadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadSpeed;
[XmlRpcMember("errorMessage")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string ErrorMessage;
} }
} }

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Linq;
using CookComputing.XmlRpc; using System.Xml.Linq;
using NLog; using System.Xml.XPath;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2 namespace NzbDrone.Core.Download.Clients.Aria2
{ {
@@ -12,46 +12,103 @@ namespace NzbDrone.Core.Download.Clients.Aria2
string GetVersion(Aria2Settings settings); string GetVersion(Aria2Settings settings);
string AddUri(Aria2Settings settings, string magnet); string AddUri(Aria2Settings settings, string magnet);
string AddTorrent(Aria2Settings settings, byte[] torrent); string AddTorrent(Aria2Settings settings, byte[] torrent);
Dictionary<string, string> GetGlobals(Aria2Settings settings);
List<Aria2Status> GetTorrents(Aria2Settings settings);
Aria2Status GetFromGID(Aria2Settings settings, string gid); Aria2Status GetFromGID(Aria2Settings settings, string gid);
} }
public interface IAria2 : IXmlRpcProxy
{
[XmlRpcMethod("aria2.getVersion")]
Aria2Version GetVersion(string token);
[XmlRpcMethod("aria2.addUri")]
string AddUri(string token, string[] uri);
[XmlRpcMethod("aria2.addTorrent")]
string AddTorrent(string token, byte[] torrent);
[XmlRpcMethod("aria2.forceRemove")]
string Remove(string token, string gid);
[XmlRpcMethod("aria2.tellStatus")]
Aria2Status GetFromGid(string token, string gid);
[XmlRpcMethod("aria2.getGlobalOption")]
XmlRpcStruct GetGlobalOption(string token);
[XmlRpcMethod("aria2.tellActive")]
Aria2Status[] GetActives(string token);
[XmlRpcMethod("aria2.tellWaiting")]
Aria2Status[] GetWaitings(string token, int offset, int num);
[XmlRpcMethod("aria2.tellStopped")]
Aria2Status[] GetStoppeds(string token, int offset, int num);
}
public class Aria2Proxy : IAria2Proxy public class Aria2Proxy : IAria2Proxy
{ {
private readonly Logger _logger; private readonly IHttpClient _httpClient;
public Aria2Proxy(Logger logger) public Aria2Proxy(IHttpClient httpClient)
{ {
_logger = logger; _httpClient = httpClient;
}
public string GetVersion(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getVersion", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var version = new Aria2Version(element);
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
var response = ExecuteRequest(settings, "aria2.tellStatus", GetToken(settings), gid);
var element = response.XPathSelectElement("./methodResponse/params/param/value");
return new Aria2Status(element);
}
private List<Aria2Status> GetTorrentsMethod(Aria2Settings settings, string method, params object[] args)
{
var allArgs = new List<object> { GetToken(settings) };
if (args.Any())
{
allArgs.AddRange(args);
}
var response = ExecuteRequest(settings, method, allArgs.ToArray());
var element = response.XPathSelectElement("./methodResponse/params/param/value/array/data");
var torrents = element?.Elements()
.Select(x => new Aria2Status(x))
.ToList()
?? new List<Aria2Status>();
return torrents;
}
public List<Aria2Status> GetTorrents(Aria2Settings settings)
{
var active = GetTorrentsMethod(settings, "aria2.tellActive");
var waiting = GetTorrentsMethod(settings, "aria2.tellWaiting", 0, 10 * 1024);
var stopped = GetTorrentsMethod(settings, "aria2.tellStopped", 0, 10 * 1024);
var items = new List<Aria2Status>();
items.AddRange(active);
items.AddRange(waiting);
items.AddRange(stopped);
return items;
}
public Dictionary<string, string> GetGlobals(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getGlobalOption", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var result = new Aria2Dict(element);
return result.Dict;
}
public string AddUri(Aria2Settings settings, string magnet)
{
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var gid = response.GetStringResponse();
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
var gid = response.GetStringResponse();
return gid;
} }
private string GetToken(Aria2Settings settings) private string GetToken(Aria2Settings settings)
@@ -59,86 +116,29 @@ namespace NzbDrone.Core.Download.Clients.Aria2
return $"token:{settings?.SecretToken}"; return $"token:{settings?.SecretToken}";
} }
private string GetURL(Aria2Settings settings) private XDocument ExecuteRequest(Aria2Settings settings, string methodName, params object[] args)
{ {
return $"http{(settings.UseSsl ? "s" : "")}://{settings.Host}:{settings.Port}{settings.RpcPath}"; var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
}
public string GetVersion(Aria2Settings settings)
{
_logger.Debug("> aria2.getVersion");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion(GetToken(settings)));
_logger.Debug("< aria2.getVersion");
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
_logger.Debug("> aria2.tellStatus");
var client = BuildClient(settings);
var found = ExecuteRequest(() => client.GetFromGid(GetToken(settings), gid));
_logger.Debug("< aria2.tellStatus");
return found;
}
public string AddUri(Aria2Settings settings, string magnet)
{
_logger.Debug("> aria2.addUri");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddUri(GetToken(settings), new[] { magnet }));
_logger.Debug("< aria2.addUri");
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
_logger.Debug("> aria2.addTorrent");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddTorrent(GetToken(settings), torrent));
_logger.Debug("< aria2.addTorrent");
return gid;
}
private IAria2 BuildClient(Aria2Settings settings)
{
var client = XmlRpcProxyGen.Create<IAria2>();
client.Url = GetURL(settings);
return client;
}
private T ExecuteRequest<T>(Func<T> task)
{
try
{ {
return task(); LogResponseContent = true,
} };
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to aria2, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to aria2, certificate validation failed.", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to aria2, please check your settings", ex); var request = requestBuilder.Call(methodName, args).Build();
var response = _httpClient.Execute(request);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new Aria2Fault(faultElement);
throw new DownloadClientException($"Aria2 returned error code {fault.FaultCode}: {fault.FaultString}");
} }
return doc;
} }
} }
} }

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
baseUrl = HttpUri.CombinePath(baseUrl, "api"); baseUrl = HttpUri.CombinePath(baseUrl, "api");
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate"); requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate");
var httpRequest = requestBuilder.Build(); var httpRequest = requestBuilder.Build();

View File

@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
var httpRequest = requestBuilder.Build(); var httpRequest = requestBuilder.Build();

View File

@@ -276,7 +276,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{ {
LogResponseContent = true, LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password) NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
}; };
return requestBuilder; return requestBuilder;
} }

View File

@@ -317,7 +317,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{ {
LogResponseContent = true, LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password) NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
}; };
return requestBuilder; return requestBuilder;
} }

View File

@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.AllowAutoRedirect = false; requestBuilder.AllowAutoRedirect = false;
return requestBuilder; return requestBuilder;

View File

@@ -0,0 +1,28 @@
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentFault
{
public RTorrentFault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").GetIntValue();
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
}

View File

@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using CookComputing.XmlRpc; using System.Xml.Linq;
using NLog; using System.Xml.XPath;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
@@ -18,122 +20,70 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void RemoveTorrent(string hash, RTorrentSettings settings); void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings); void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, RTorrentSettings settings); bool HasHashTorrent(string hash, RTorrentSettings settings);
} void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
public interface IRTorrent : IXmlRpcProxy
{
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
[XmlRpcMethod("d.erase")]
int Remove(string hash);
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("system.client_version")]
string GetVersion();
} }
public class RTorrentProxy : IRTorrentProxy public class RTorrentProxy : IRTorrentProxy
{ {
private readonly Logger _logger; private readonly IHttpClient _httpClient;
public RTorrentProxy(Logger logger) public RTorrentProxy(IHttpClient httpClient)
{ {
_logger = logger; _httpClient = httpClient;
} }
public string GetVersion(RTorrentSettings settings) public string GetVersion(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: system.client_version"); var document = ExecuteRequest(settings, "system.client_version");
var client = BuildClient(settings); return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
var version = ExecuteRequest(() => client.GetVersion());
return version;
} }
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings) public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.multicall2"); var document = ExecuteRequest(settings,
"d.multicall2",
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=", //long
"d.timestamp.finished="); // long (unix timestamp)
var client = BuildClient(settings); var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
var ret = ExecuteRequest(() => client.TorrentMulticall( ?.Elements()
"", .Select(x => new RTorrentTorrent(x))
"", .ToList()
"d.name=", // string ?? new List<RTorrentTorrent>();
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=")); //long
_logger.Trace(ret.ToJson()); return torrents;
var items = new List<RTorrentTorrent>();
foreach (object[] torrent in ret)
{
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
var item = new RTorrentTorrent();
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
items.Add(item);
}
return items;
} }
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", torrentUrl };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.normal");
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.start");
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
}
});
if (response != 0) XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.normal", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl); throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
} }
@@ -141,22 +91,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", fileContent };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.raw");
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.raw_start");
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
}
});
if (response != 0) XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.raw", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", fileName); throw new DownloadClientException("Could not add torrent: {0}.", fileName);
} }
@@ -164,25 +113,29 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings) public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.custom1.set"); var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
var client = BuildClient(settings); if (response.GetStringResponse() != label)
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
{ {
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label); throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
} }
} }
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{
var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
}
}
public void RemoveTorrent(string hash, RTorrentSettings settings) public void RemoveTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.erase"); var response = ExecuteRequest(settings, "d.erase", hash);
var client = BuildClient(settings); if (response.GetIntResponse() != 0)
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0)
{ {
throw new DownloadClientException("Could not remove torrent: {0}.", hash); throw new DownloadClientException("Could not remove torrent: {0}.", hash);
} }
@@ -190,13 +143,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public bool HasHashTorrent(string hash, RTorrentSettings settings) public bool HasHashTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try try
{ {
var name = ExecuteRequest(() => client.GetName(hash)); var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
if (name.IsNullOrWhiteSpace()) if (name.IsNullOrWhiteSpace())
{ {
@@ -235,45 +185,34 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray(); return result.ToArray();
} }
private IRTorrent BuildClient(RTorrentSettings settings) private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
{ {
var client = XmlRpcProxyGen.Create<IRTorrent>(); var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
client.Url = string.Format(@"{0}://{1}:{2}/{3}", LogResponseContent = true,
settings.UseSsl ? "https" : "http", };
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
if (!settings.Username.IsNullOrWhiteSpace()) if (!settings.Username.IsNullOrWhiteSpace())
{ {
client.Credentials = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
} }
return client; var request = requestBuilder.Call(methodName, args).Build();
}
private T ExecuteRequest<T>(Func<T> task) var response = _httpClient.Execute(request);
{
try
{
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex); var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new RTorrentFault(faultElement);
throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
} }
return doc;
} }
} }
} }

View File

@@ -1,7 +1,35 @@
namespace NzbDrone.Core.Download.Clients.RTorrent using System;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
public class RTorrentTorrent public class RTorrentTorrent
{ {
public RTorrentTorrent()
{
}
public RTorrentTorrent(XElement element)
{
var data = element.Descendants("value").ToList();
Name = data[0].GetStringValue();
Hash = data[1].GetStringValue();
Path = data[2].GetStringValue();
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
TotalSize = data[4].GetLongValue();
RemainingSize = data[5].GetLongValue();
DownRate = data[6].GetLongValue();
Ratio = data[7].GetLongValue();
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
IsActive = Convert.ToBoolean(data[9].GetLongValue());
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
FinishedTime = data[11].GetLongValue();
}
public string Name { get; set; } public string Name { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string Path { get; set; } public string Path { get; set; }
@@ -10,6 +38,7 @@
public long RemainingSize { get; set; } public long RemainingSize { get; set; }
public long DownRate { get; set; } public long DownRate { get; set; }
public long Ratio { get; set; } public long Ratio { get; set; }
public long FinishedTime { get; set; }
public bool IsFinished { get; set; } public bool IsFinished { get; set; }
public bool IsOpen { get; set; } public bool IsOpen { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
return requestBuilder; return requestBuilder;
} }

View File

@@ -0,0 +1,54 @@
using System.Xml.Linq;
using System.Xml.XPath;
namespace NzbDrone.Core.Download.Extensions
{
internal static class XmlExtensions
{
public static string GetStringValue(this XElement element)
{
return element.ElementAsString("string");
}
public static long GetLongValue(this XElement element)
{
return element.ElementAsLong("i8");
}
public static int GetIntValue(this XElement element)
{
return element.ElementAsInt("i4");
}
public static string ElementAsString(this XElement element, XName name, bool trim = false)
{
var el = element.Element(name);
return string.IsNullOrWhiteSpace(el?.Value)
? null
: (trim ? el.Value.Trim() : el.Value);
}
public static long ElementAsLong(this XElement element, XName name)
{
var el = element.Element(name);
return long.TryParse(el?.Value, out long value) ? value : default;
}
public static int ElementAsInt(this XElement element, XName name)
{
var el = element.Element(name);
return int.TryParse(el?.Value, out int value) ? value : default(int);
}
public static int GetIntResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetIntValue();
}
public static string GetStringResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetStringValue();
}
}
}

View File

@@ -1,21 +0,0 @@
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareCaptchaException : NzbDroneException
{
public HttpResponse Response { get; set; }
public CloudFlareCaptchaRequest CaptchaRequest { get; set; }
public CloudFlareCaptchaException(HttpResponse response, CloudFlareCaptchaRequest captchaRequest)
: base("Unable to access {0}, blocked by CloudFlare CAPTCHA. Likely due to shared-IP VPN.", response.Request.Url.Host)
{
Response = response;
CaptchaRequest = captchaRequest;
}
public bool IsExpired => Response.Request.Cookies.ContainsKey("cf_clearance");
}
}

View File

@@ -1,15 +0,0 @@
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareCaptchaRequest
{
public string Host { get; set; }
public string SiteKey { get; set; }
public string Ray { get; set; }
public string SecretToken { get; set; }
public HttpUri ResponseUrl { get; set; }
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareDetectionService
{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx", "ddos-guard" };
private readonly Logger _logger;
public CloudFlareDetectionService(Logger logger)
{
_logger = logger;
}
public static bool IsCloudflareProtected(HttpResponse response)
{
if (!response.Headers.Any(i => i.Key != null && i.Key.ToLower() == "server" && CloudflareServerNames.Contains(i.Value.ToLower())))
{
return false;
}
// detect CloudFlare and DDoS-GUARD
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden))
{
return true; // Defected CloudFlare and DDoS-GUARD
}
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary == "Accept-Encoding,User-Agent" &&
response.Headers.ContentEncoding == "" &&
response.Content.ToLower().Contains("ddos"))
{
return true;
}
return false;
}
}
}

View File

@@ -1,54 +0,0 @@
using System.Net;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareHttpInterceptor : IHttpRequestInterceptor
{
private const string _cloudFlareChallengeScript = "cdn-cgi/scripts/cf.challenge.js";
private readonly Logger _logger;
private static readonly Regex _cloudFlareRegex = new Regex(@"data-ray=""(?<Ray>[\w-_]+)"".*?data-sitekey=""(?<SiteKey>[\w-_]+)"".*?data-stoken=""(?<SecretToken>[\w-_]+)""", RegexOptions.Compiled);
public CloudFlareHttpInterceptor(Logger logger)
{
_logger = logger;
}
public HttpRequest PreRequest(HttpRequest request)
{
return request;
}
public HttpResponse PostResponse(HttpResponse response)
{
if (response.StatusCode == HttpStatusCode.Forbidden && response.Content.Contains(_cloudFlareChallengeScript))
{
_logger.Debug("CloudFlare CAPTCHA block on {0}", response.Request.Url);
throw new CloudFlareCaptchaException(response, CreateCaptchaRequest(response));
}
return response;
}
private CloudFlareCaptchaRequest CreateCaptchaRequest(HttpResponse response)
{
var match = _cloudFlareRegex.Match(response.Content);
if (!match.Success)
{
return null;
}
return new CloudFlareCaptchaRequest
{
Host = response.Request.Url.Host,
SiteKey = match.Groups["SiteKey"].Value,
Ray = match.Groups["Ray"].Value,
SecretToken = match.Groups["SecretToken"].Value,
ResponseUrl = response.Request.Url + new HttpUri("/cdn-cgi/l/chk_captcha")
};
}
}
}

View File

@@ -0,0 +1,16 @@
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareProtectionException : NzbDroneException
{
public HttpResponse Response { get; set; }
public CloudFlareProtectionException(HttpResponse response)
: base("Unable to access {0}, blocked by CloudFlare Protection.", response.Request.Url.Host)
{
Response = response;
}
}
}

View File

@@ -11,6 +11,7 @@ using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -18,7 +19,6 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
{ {
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings> public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{ {
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx" };
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager) public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public override HttpResponse PostResponse(HttpResponse response) public override HttpResponse PostResponse(HttpResponse response)
{ {
if (!IsCloudflareProtected(response)) if (!CloudFlareDetectionService.IsCloudflareProtected(response))
{ {
_logger.Debug("CF Protection not detected, returning original response"); _logger.Debug("CF Protection not detected, returning original response");
return response; return response;
@@ -53,14 +53,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
var flaresolverrResponse = _httpClient.Execute(GenerateFlareSolverrRequest(response.Request)); var flaresolverrResponse = _httpClient.Execute(GenerateFlareSolverrRequest(response.Request));
FlareSolverrResponse result = null;
if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError) if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError)
{ {
throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + response.StatusCode); throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + response.StatusCode);
} }
result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content); var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content);
var newRequest = response.Request; var newRequest = response.Request;
@@ -70,26 +68,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
InjectCookies(newRequest, result); InjectCookies(newRequest, result);
//Request again with User-Agent and Cookies from Flaresolvrr //Request again with User-Agent and Cookies from Flaresolverr
var finalResponse = _httpClient.Execute(newRequest); var finalResponse = _httpClient.Execute(newRequest);
return finalResponse; return finalResponse;
} }
private static bool IsCloudflareProtected(HttpResponse response)
{
// check status code
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden))
{
// check response headers
return response.Headers.Any(i =>
i.Key != null && i.Key.ToLower() == "server" && CloudflareServerNames.Contains(i.Value.ToLower()));
}
return false;
}
private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse) private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse)
{ {
var rCookies = flareSolverrResponse.Solution.Cookies; var rCookies = flareSolverrResponse.Solution.Cookies;
@@ -128,9 +112,9 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
} }
else if (request.Method == HttpMethod.Post) else if (request.Method == HttpMethod.Post)
{ {
var contentTypeType = request.Headers.ContentType; var contentTypeType = request.Headers.ContentType.ToLower() ?? "<null>";
if (contentTypeType == "application/x-www-form-urlencoded") if (contentTypeType.Contains("application/x-www-form-urlencoded"))
{ {
var contentTypeValue = request.Headers.ContentType.ToString(); var contentTypeValue = request.Headers.ContentType.ToString();
var postData = request.GetContent(); var postData = request.GetContent();
@@ -149,7 +133,8 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
UserAgent = userAgent UserAgent = userAgent
}; };
} }
else if (contentTypeType.Contains("multipart/form-data")) else if (contentTypeType.Contains("multipart/form-data")
|| contentTypeType.Contains("text/html"))
{ {
//TODO Implement - check if we just need to pass the content-type with the relevant headers //TODO Implement - check if we just need to pass the content-type with the relevant headers
throw new FlareSolverrException("Unimplemented POST Content-Type: " + request.Headers.ContentType); throw new FlareSolverrException("Unimplemented POST Content-Type: " + request.Headers.ContentType);
@@ -169,9 +154,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
newRequest.Headers.ContentType = "application/json"; newRequest.Headers.ContentType = "application/json";
newRequest.Method = HttpMethod.Post; newRequest.Method = HttpMethod.Post;
newRequest.LogResponseContent = true;
newRequest.SetContent(req.ToJson()); newRequest.SetContent(req.ToJson());
_logger.Debug("Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url);
return newRequest; return newRequest;
} }

View File

@@ -6,6 +6,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{ {
public string Author { get; set; } public string Author { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Publisher { get; set; }
public int? Year { get; set; }
public string Genre { get; set; }
public override bool RssSearch public override bool RssSearch
{ {

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int? TmdbId { get; set; } public int? TmdbId { get; set; }
public int? TraktId { get; set; } public int? TraktId { get; set; }
public int? DoubanId { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public string Genre { get; set; } public string Genre { get; set; }

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Artist { get; set; } public string Artist { get; set; }
public string Label { get; set; } public string Label { get; set; }
public string Genre { get; set; } public string Genre { get; set; }
public string Track { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public override bool RssSearch public override bool RssSearch

View File

@@ -17,6 +17,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? TvMazeId { get; set; } public int? TvMazeId { get; set; }
public int? TraktId { get; set; } public int? TraktId { get; set; }
public int? TmdbId { get; set; } public int? TmdbId { get; set; }
public int? DoubanId { get; set; }
public int? Year { get; set; }
public string Genre { get; set; }
public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim(); public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim();
public string EpisodeSearchString => GetEpisodeSearchString(); public string EpisodeSearchString => GetEpisodeSearchString();

View File

@@ -4,10 +4,10 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public class NewznabRequest public class NewznabRequest
{ {
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string t { get; set; } public string t { get; set; }
public string q { get; set; } public string q { get; set; }
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.IndexerSearch
public int? tvmazeid { get; set; } public int? tvmazeid { get; set; }
public int? traktid { get; set; } public int? traktid { get; set; }
public int? tvdbid { get; set; } public int? tvdbid { get; set; }
public int? doubanid { get; set; }
public int? season { get; set; } public int? season { get; set; }
public string ep { get; set; } public string ep { get; set; }
public string album { get; set; } public string album { get; set; }
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.IndexerSearch
public string genre { get; set; } public string genre { get; set; }
public string author { get; set; } public string author { get; set; }
public string title { get; set; } public string title { get; set; }
public string publisher { get; set; }
public string configured { get; set; } public string configured { get; set; }
public string source { get; set; } public string source { get; set; }
public string host { get; set; } public string host { get; set; }
@@ -49,6 +51,16 @@ namespace NzbDrone.Core.IndexerSearch
tvdbid = int.TryParse(match.Groups["tvdbid"].Value, out var tvdb) ? tvdb : null; tvdbid = int.TryParse(match.Groups["tvdbid"].Value, out var tvdb) ? tvdb : null;
} }
if (match.Groups["tmdbid"].Success)
{
tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["season"].Success) if (match.Groups["season"].Success)
{ {
season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null; season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null;
@@ -79,6 +91,11 @@ namespace NzbDrone.Core.IndexerSearch
tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null; tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null;
} }
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["imdbid"].Success) if (match.Groups["imdbid"].Success)
{ {
imdbid = match.Groups["imdbid"].Value; imdbid = match.Groups["imdbid"].Value;
@@ -104,6 +121,11 @@ namespace NzbDrone.Core.IndexerSearch
album = match.Groups["album"].Value; album = match.Groups["album"].Value;
} }
if (match.Groups["track"].Success)
{
track = match.Groups["track"].Value;
}
if (match.Groups["label"].Success) if (match.Groups["label"].Success)
{ {
label = match.Groups["label"].Value; label = match.Groups["label"].Value;
@@ -129,6 +151,11 @@ namespace NzbDrone.Core.IndexerSearch
title = match.Groups["title"].Value; title = match.Groups["title"].Value;
} }
if (match.Groups["publisher"].Success)
{
publisher = match.Groups["publisher"].Value;
}
q = q.Replace(match.Value, "").Trim(); q = q.Replace(match.Value, "").Trim();
} }
} }

View File

@@ -90,8 +90,8 @@ namespace NzbDrone.Core.IndexerSearch
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")), 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.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.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.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol),
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol), r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol),
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol), r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
GetNabElement("rageid", r.TvRageId, protocol), GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol), GetNabElement("tvdbid", r.TvdbId, protocol),
@@ -107,6 +107,8 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol), GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol), GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol),
GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol), GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol),
GetNabElement("label", RemoveInvalidXMLChars(r.Label), protocol),
GetNabElement("track", RemoveInvalidXMLChars(r.Track), protocol),
GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol), GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol),
GetNabElement("minimumratio", t.MinimumRatio, protocol), GetNabElement("minimumratio", t.MinimumRatio, protocol),
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol), GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),

View File

@@ -1,13 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using NLog; using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Analytics; using NzbDrone.Core.Analytics;
using NzbDrone.Core.Indexers.Events; using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
@@ -16,26 +20,48 @@ namespace NzbDrone.Core.IndexerSearch
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _requestBuilder; private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IAnalyticsService _analyticsService; private readonly IAnalyticsService _analyticsService;
private readonly Debouncer _debouncer;
private readonly Logger _logger; private readonly Logger _logger;
private readonly List<ReleaseInfo> _pendingUpdates;
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger) public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
{ {
_debouncer = new Debouncer(SendReleases, TimeSpan.FromMinutes(10));
_analyticsService = analyticsService; _analyticsService = analyticsService;
_requestBuilder = requestBuilder.Releases; _requestBuilder = requestBuilder.Releases;
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger; _logger = logger;
_pendingUpdates = new List<ReleaseInfo>();
} }
public void HandleAsync(IndexerQueryEvent message) public void HandleAsync(IndexerQueryEvent message)
{ {
if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null) if (message.QueryResult?.Releases != null)
{ {
lock (_pendingUpdates)
{
_pendingUpdates.AddRange(message.QueryResult.Releases.Where(r => r.Title.IsNotNullOrWhiteSpace()));
}
_debouncer.Execute();
}
}
public void SendReleases()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.ToArray();
_pendingUpdates.Clear();
var request = _requestBuilder.Create().Resource("release/push").Build(); var request = _requestBuilder.Create().Resource("release/push").Build();
request.Method = HttpMethod.Post; request.Method = HttpMethod.Post;
request.Headers.ContentType = "application/json"; request.Headers.ContentType = "application/json";
request.SuppressHttpError = true; request.SuppressHttpError = true;
request.LogHttpError = false;
var body = message.QueryResult.Releases.Select(x => new var body = pendingUpdates.DistinctBy(r => r.Title).Select(x => new
{ {
Title = x.Title, Title = x.Title,
Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(), Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(),

View File

@@ -59,6 +59,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.ImdbId = request.imdbid; searchSpec.ImdbId = request.imdbid;
searchSpec.TmdbId = request.tmdbid; searchSpec.TmdbId = request.tmdbid;
searchSpec.TraktId = request.traktid; searchSpec.TraktId = request.traktid;
searchSpec.DoubanId = request.doubanid;
searchSpec.Year = request.year; searchSpec.Year = request.year;
searchSpec.Genre = request.genre; searchSpec.Genre = request.genre;
@@ -73,6 +74,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Album = request.album; searchSpec.Album = request.album;
searchSpec.Label = request.label; searchSpec.Label = request.label;
searchSpec.Genre = request.genre; searchSpec.Genre = request.genre;
searchSpec.Track = request.track;
searchSpec.Year = request.year; searchSpec.Year = request.year;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
@@ -88,8 +90,11 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.ImdbId = request.imdbid; searchSpec.ImdbId = request.imdbid;
searchSpec.TraktId = request.traktid; searchSpec.TraktId = request.traktid;
searchSpec.TmdbId = request.tmdbid; searchSpec.TmdbId = request.tmdbid;
searchSpec.DoubanId = request.doubanid;
searchSpec.RId = request.rid; searchSpec.RId = request.rid;
searchSpec.TvMazeId = request.tvmazeid; searchSpec.TvMazeId = request.tvmazeid;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
} }
@@ -100,6 +105,9 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Author = request.author; searchSpec.Author = request.author;
searchSpec.Title = request.title; searchSpec.Title = request.title;
searchSpec.Publisher = request.publisher;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
} }

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */ /* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master"; private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 6; private const int DEFINITION_VERSION = 7;
//Used when moving yml to C# //Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>() private readonly List<string> _defintionBlocklist = new List<string>()

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -493,7 +494,7 @@ namespace NzbDrone.Core.Indexers.Definitions
// Throw common http errors here before we try to parse // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if ((int)releaseResponse.HttpResponse.StatusCode == 429) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
@@ -307,7 +308,7 @@ namespace NzbDrone.Core.Indexers.Definitions
// Throw common http errors here before we try to parse // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if ((int)releaseResponse.HttpResponse.StatusCode == 429) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
} }

View File

@@ -40,6 +40,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[JsonProperty(PropertyName = "video_quality")] [JsonProperty(PropertyName = "video_quality")]
public string VideoQuality { get; set; } public string VideoQuality { get; set; }
public string Type { get; set; } public string Type { get; set; }
public List<AvistazLanguage> Audio { get; set; }
public List<AvistazLanguage> Subtitle { get; set; }
}
public class AvistazLanguage
{
public int Id { get; set; }
public string Language { get; set; }
} }
public class AvistazResponse public class AvistazResponse

View File

@@ -66,6 +66,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
UploadVolumeFactor = row.UploadMultiply, UploadVolumeFactor = row.UploadMultiply,
MinimumRatio = 1, MinimumRatio = 1,
MinimumSeedTime = 172800, // 48 hours MinimumSeedTime = 172800, // 48 hours
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
}; };
if (row.MovieTvinfo != null) if (row.MovieTvinfo != null)

View File

@@ -111,7 +111,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new HttpRequest(searchUrl, HttpAccept.Json); var request = new HttpRequest(searchUrl, HttpAccept.Json);
request.Headers.Add("Content-type", "application/json"); request.Headers.ContentType = "application/json";
request.Method = HttpMethod.Post; request.Method = HttpMethod.Post;
request.SetContent(body.ToJson()); request.SetContent(body.ToJson());

View File

@@ -194,7 +194,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
throw new ReleaseUnavailableException("Downloading torrent failed", ex); throw new ReleaseUnavailableException("Downloading torrent failed", ex);
} }
if ((int)ex.Response.StatusCode == 429) if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
_logger.Error("API Grab Limit reached for {0}", request.Url.FullUri); _logger.Error("API Grab Limit reached for {0}", request.Url.FullUri);
} }

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories(); protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories();
protected readonly List<string> _defaultCategories = new List<string>(); protected readonly List<string> _defaultCategories = new List<string>();
protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "rageid", "tmdbid", "tvdbid", "poster", "banner", "description", "doubanid" }; protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "tmdbid", "rageid", "tvdbid", "tvmazeid", "traktid", "doubanid", "poster", "banner", "description", "genre" };
protected static readonly string[] _SupportedLogicFunctions = protected static readonly string[] _SupportedLogicFunctions =
{ {

View File

@@ -145,14 +145,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
throw new CardigannException(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value ?? "<null>", ex.Message)); throw new CardigannException(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value ?? "<null>", ex.Message));
} }
}
var filters = search.Rows.Filters; var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row); var skipRelease = ParseRowFilters(filters, release, variables, row);
if (skipRelease) if (skipRelease)
{ {
continue; continue;
}
} }
releases.Add(release); releases.Add(release);
@@ -279,7 +279,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
} }
var filters = search.Rows.Filters; var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row); var skipRelease = ParseRowFilters(filters, release, variables, row.ToHtmlPretty());
if (skipRelease) if (skipRelease)
{ {
@@ -594,8 +594,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.PosterUrl; value = release.PosterUrl;
break; break;
case "genre": case "genre":
release.Genres = release.Genres.Union(value.Split(',')).ToList(); char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
value = release.Genres.ToString(); release.Genres = release.Genres.Union(value.Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries)).ToList();
value = string.Join(", ", release.Genres);
break; break;
case "year": case "year":
release.Year = ParseUtil.CoerceInt(value); release.Year = ParseUtil.CoerceInt(value);
@@ -607,12 +608,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
case "booktitle": case "booktitle":
release.BookTitle = value; release.BookTitle = value;
break; break;
case "publisher":
release.Publisher = value;
break;
case "artist": case "artist":
release.Artist = value; release.Artist = value;
break; break;
case "album": case "album":
release.Album = value; release.Album = value;
break; break;
case "label":
release.Label = value;
break;
case "track":
release.Track = value;
break;
default: default:
break; break;
} }
@@ -644,6 +654,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
// for debugging // for debugging
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString())); _logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString()));
break; break;
case "validate":
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
var args = (string)filter.Args;
var argsList = args.ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
var validList = argsList.ToList();
var validIntersect = validList.Intersect(row.ToString().ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries)).ToList();
row = string.Join(", ", validIntersect);
break;
default: default:
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name)); _logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
break; break;

View File

@@ -49,10 +49,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Movie"] = null; variables[".Query.Movie"] = null;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null; variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.IMDBID"] = searchCriteria.FullImdbId; variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId; variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null; variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null; variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -70,7 +72,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Album"] = searchCriteria.Album; variables[".Query.Album"] = searchCriteria.Album;
variables[".Query.Artist"] = searchCriteria.Artist; variables[".Query.Artist"] = searchCriteria.Artist;
variables[".Query.Label"] = searchCriteria.Label; variables[".Query.Label"] = searchCriteria.Label;
variables[".Query.Track"] = null; variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.Track"] = searchCriteria.Track;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -88,6 +92,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Series"] = null; variables[".Query.Series"] = null;
variables[".Query.Ep"] = searchCriteria.Episode; variables[".Query.Ep"] = searchCriteria.Episode;
variables[".Query.Season"] = searchCriteria.Season?.ToString() ?? null; variables[".Query.Season"] = searchCriteria.Season?.ToString() ?? null;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.IMDBID"] = searchCriteria.FullImdbId; variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId; variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TVDBID"] = searchCriteria.TvdbId?.ToString() ?? null; variables[".Query.TVDBID"] = searchCriteria.TvdbId?.ToString() ?? null;
@@ -95,6 +101,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.TVRageID"] = searchCriteria.RId?.ToString() ?? null; variables[".Query.TVRageID"] = searchCriteria.RId?.ToString() ?? null;
variables[".Query.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null; variables[".Query.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null; variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
variables[".Query.Episode"] = searchCriteria.EpisodeSearchString; variables[".Query.Episode"] = searchCriteria.EpisodeSearchString;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -112,6 +119,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Author"] = searchCriteria.Author; variables[".Query.Author"] = searchCriteria.Author;
variables[".Query.Title"] = searchCriteria.Title; variables[".Query.Title"] = searchCriteria.Title;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Publisher"] = searchCriteria.Publisher;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -343,6 +353,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding Encoding = _encoding
}; };
requestBuilder.SetCookies(Cookies);
requestBuilder.Headers.Add("Referer", loginUrl); requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition); var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);

View File

@@ -1,21 +1,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using AngleSharp.Html.Parser; using System.Threading.Tasks;
using FluentValidation; using FluentValidation;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
@@ -38,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator() public override IIndexerRequestGenerator GetRequestGenerator()
{ {
return new GazelleGamesRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; return new GazelleGamesRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient };
} }
public override IParseIndexerResponse GetParser() public override IParseIndexerResponse GetParser()
@@ -46,38 +47,27 @@ namespace NzbDrone.Core.Indexers.Definitions
return new GazelleGamesParser(Settings, Capabilities.Categories); return new GazelleGamesParser(Settings, Capabilities.Categories);
} }
protected override IDictionary<string, string> GetCookies()
{
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (httpResponse.HasHttpRedirect && httpResponse.RedirectUrl.EndsWith("login.php"))
{
return true;
}
return false;
}
private IndexerCapabilities SetCapabilities() private IndexerCapabilities SetCapabilities()
{ {
var caps = new IndexerCapabilities var caps = new IndexerCapabilities
{ {
}; };
// Apple
caps.Categories.AddCategoryMapping("Mac", NewznabStandardCategory.ConsoleOther, "Mac"); caps.Categories.AddCategoryMapping("Mac", NewznabStandardCategory.ConsoleOther, "Mac");
caps.Categories.AddCategoryMapping("iOS", NewznabStandardCategory.PCMobileiOS, "iOS"); caps.Categories.AddCategoryMapping("iOS", NewznabStandardCategory.PCMobileiOS, "iOS");
caps.Categories.AddCategoryMapping("Apple Bandai Pippin", NewznabStandardCategory.ConsoleOther, "Apple Bandai Pippin"); caps.Categories.AddCategoryMapping("Apple Bandai Pippin", NewznabStandardCategory.ConsoleOther, "Apple Bandai Pippin");
// Google
caps.Categories.AddCategoryMapping("Android", NewznabStandardCategory.PCMobileAndroid, "Android"); caps.Categories.AddCategoryMapping("Android", NewznabStandardCategory.PCMobileAndroid, "Android");
// Microsoft
caps.Categories.AddCategoryMapping("DOS", NewznabStandardCategory.PCGames, "DOS"); caps.Categories.AddCategoryMapping("DOS", NewznabStandardCategory.PCGames, "DOS");
caps.Categories.AddCategoryMapping("Windows", NewznabStandardCategory.PCGames, "Windows"); caps.Categories.AddCategoryMapping("Windows", NewznabStandardCategory.PCGames, "Windows");
caps.Categories.AddCategoryMapping("Xbox", NewznabStandardCategory.ConsoleXBox, "Xbox"); caps.Categories.AddCategoryMapping("Xbox", NewznabStandardCategory.ConsoleXBox, "Xbox");
caps.Categories.AddCategoryMapping("Xbox 360", NewznabStandardCategory.ConsoleXBox360, "Xbox 360"); caps.Categories.AddCategoryMapping("Xbox 360", NewznabStandardCategory.ConsoleXBox360, "Xbox 360");
// Nintendo
caps.Categories.AddCategoryMapping("Game Boy", NewznabStandardCategory.ConsoleOther, "Game Boy"); caps.Categories.AddCategoryMapping("Game Boy", NewznabStandardCategory.ConsoleOther, "Game Boy");
caps.Categories.AddCategoryMapping("Game Boy Advance", NewznabStandardCategory.ConsoleOther, "Game Boy Advance"); caps.Categories.AddCategoryMapping("Game Boy Advance", NewznabStandardCategory.ConsoleOther, "Game Boy Advance");
caps.Categories.AddCategoryMapping("Game Boy Color", NewznabStandardCategory.ConsoleOther, "Game Boy Color"); caps.Categories.AddCategoryMapping("Game Boy Color", NewznabStandardCategory.ConsoleOther, "Game Boy Color");
@@ -93,6 +83,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping("Wii", NewznabStandardCategory.ConsoleWii, "Wii"); caps.Categories.AddCategoryMapping("Wii", NewznabStandardCategory.ConsoleWii, "Wii");
caps.Categories.AddCategoryMapping("Wii U", NewznabStandardCategory.ConsoleWiiU, "Wii U"); caps.Categories.AddCategoryMapping("Wii U", NewznabStandardCategory.ConsoleWiiU, "Wii U");
// Sony
caps.Categories.AddCategoryMapping("PlayStation 1", NewznabStandardCategory.ConsoleOther, "PlayStation 1"); caps.Categories.AddCategoryMapping("PlayStation 1", NewznabStandardCategory.ConsoleOther, "PlayStation 1");
caps.Categories.AddCategoryMapping("PlayStation 2", NewznabStandardCategory.ConsoleOther, "PlayStation 2"); caps.Categories.AddCategoryMapping("PlayStation 2", NewznabStandardCategory.ConsoleOther, "PlayStation 2");
caps.Categories.AddCategoryMapping("PlayStation 3", NewznabStandardCategory.ConsolePS3, "PlayStation 3"); caps.Categories.AddCategoryMapping("PlayStation 3", NewznabStandardCategory.ConsolePS3, "PlayStation 3");
@@ -100,6 +91,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping("PlayStation Portable", NewznabStandardCategory.ConsolePSP, "PlayStation Portable"); caps.Categories.AddCategoryMapping("PlayStation Portable", NewznabStandardCategory.ConsolePSP, "PlayStation Portable");
caps.Categories.AddCategoryMapping("PlayStation Vita", NewznabStandardCategory.ConsolePSVita, "PlayStation Vita"); caps.Categories.AddCategoryMapping("PlayStation Vita", NewznabStandardCategory.ConsolePSVita, "PlayStation Vita");
// Sega
caps.Categories.AddCategoryMapping("Dreamcast", NewznabStandardCategory.ConsoleOther, "Dreamcast"); caps.Categories.AddCategoryMapping("Dreamcast", NewznabStandardCategory.ConsoleOther, "Dreamcast");
caps.Categories.AddCategoryMapping("Game Gear", NewznabStandardCategory.ConsoleOther, "Game Gear"); caps.Categories.AddCategoryMapping("Game Gear", NewznabStandardCategory.ConsoleOther, "Game Gear");
caps.Categories.AddCategoryMapping("Master System", NewznabStandardCategory.ConsoleOther, "Master System"); caps.Categories.AddCategoryMapping("Master System", NewznabStandardCategory.ConsoleOther, "Master System");
@@ -108,6 +100,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping("Saturn", NewznabStandardCategory.ConsoleOther, "Saturn"); caps.Categories.AddCategoryMapping("Saturn", NewznabStandardCategory.ConsoleOther, "Saturn");
caps.Categories.AddCategoryMapping("SG-1000", NewznabStandardCategory.ConsoleOther, "SG-1000"); caps.Categories.AddCategoryMapping("SG-1000", NewznabStandardCategory.ConsoleOther, "SG-1000");
// Atari
caps.Categories.AddCategoryMapping("Atari 2600", NewznabStandardCategory.ConsoleOther, "Atari 2600"); caps.Categories.AddCategoryMapping("Atari 2600", NewznabStandardCategory.ConsoleOther, "Atari 2600");
caps.Categories.AddCategoryMapping("Atari 5200", NewznabStandardCategory.ConsoleOther, "Atari 5200"); caps.Categories.AddCategoryMapping("Atari 5200", NewznabStandardCategory.ConsoleOther, "Atari 5200");
caps.Categories.AddCategoryMapping("Atari 7800", NewznabStandardCategory.ConsoleOther, "Atari 7800"); caps.Categories.AddCategoryMapping("Atari 7800", NewznabStandardCategory.ConsoleOther, "Atari 7800");
@@ -115,24 +108,31 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping("Atari Lynx", NewznabStandardCategory.ConsoleOther, "Atari Lynx"); caps.Categories.AddCategoryMapping("Atari Lynx", NewznabStandardCategory.ConsoleOther, "Atari Lynx");
caps.Categories.AddCategoryMapping("Atari ST", NewznabStandardCategory.ConsoleOther, "Atari ST"); caps.Categories.AddCategoryMapping("Atari ST", NewznabStandardCategory.ConsoleOther, "Atari ST");
// Amstrad
caps.Categories.AddCategoryMapping("Amstrad CPC", NewznabStandardCategory.ConsoleOther, "Amstrad CPC"); caps.Categories.AddCategoryMapping("Amstrad CPC", NewznabStandardCategory.ConsoleOther, "Amstrad CPC");
// Sinclair
caps.Categories.AddCategoryMapping("ZX Spectrum", NewznabStandardCategory.ConsoleOther, "ZX Spectrum"); caps.Categories.AddCategoryMapping("ZX Spectrum", NewznabStandardCategory.ConsoleOther, "ZX Spectrum");
// Spectravideo
caps.Categories.AddCategoryMapping("MSX", NewznabStandardCategory.ConsoleOther, "MSX"); caps.Categories.AddCategoryMapping("MSX", NewznabStandardCategory.ConsoleOther, "MSX");
caps.Categories.AddCategoryMapping("MSX 2", NewznabStandardCategory.ConsoleOther, "MSX 2"); caps.Categories.AddCategoryMapping("MSX 2", NewznabStandardCategory.ConsoleOther, "MSX 2");
// Tiger
caps.Categories.AddCategoryMapping("Game.com", NewznabStandardCategory.ConsoleOther, "Game.com"); caps.Categories.AddCategoryMapping("Game.com", NewznabStandardCategory.ConsoleOther, "Game.com");
caps.Categories.AddCategoryMapping("Gizmondo", NewznabStandardCategory.ConsoleOther, "Gizmondo"); caps.Categories.AddCategoryMapping("Gizmondo", NewznabStandardCategory.ConsoleOther, "Gizmondo");
// VTech
caps.Categories.AddCategoryMapping("V.Smile", NewznabStandardCategory.ConsoleOther, "V.Smile"); caps.Categories.AddCategoryMapping("V.Smile", NewznabStandardCategory.ConsoleOther, "V.Smile");
caps.Categories.AddCategoryMapping("CreatiVision", NewznabStandardCategory.ConsoleOther, "CreatiVision"); caps.Categories.AddCategoryMapping("CreatiVision", NewznabStandardCategory.ConsoleOther, "CreatiVision");
// Tabletop Games
caps.Categories.AddCategoryMapping("Board Game", NewznabStandardCategory.ConsoleOther, "Board Game"); caps.Categories.AddCategoryMapping("Board Game", NewznabStandardCategory.ConsoleOther, "Board Game");
caps.Categories.AddCategoryMapping("Card Game", NewznabStandardCategory.ConsoleOther, "Card Game"); caps.Categories.AddCategoryMapping("Card Game", NewznabStandardCategory.ConsoleOther, "Card Game");
caps.Categories.AddCategoryMapping("Miniature Wargames", NewznabStandardCategory.ConsoleOther, "Miniature Wargames"); caps.Categories.AddCategoryMapping("Miniature Wargames", NewznabStandardCategory.ConsoleOther, "Miniature Wargames");
caps.Categories.AddCategoryMapping("Pen and Paper RPG", NewznabStandardCategory.ConsoleOther, "Pen and Paper RPG"); caps.Categories.AddCategoryMapping("Pen and Paper RPG", NewznabStandardCategory.ConsoleOther, "Pen and Paper RPG");
// Other
caps.Categories.AddCategoryMapping("3DO", NewznabStandardCategory.ConsoleOther, "3DO"); caps.Categories.AddCategoryMapping("3DO", NewznabStandardCategory.ConsoleOther, "3DO");
caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan"); caps.Categories.AddCategoryMapping("Bandai WonderSwan", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan");
caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color"); caps.Categories.AddCategoryMapping("Bandai WonderSwan Color", NewznabStandardCategory.ConsoleOther, "Bandai WonderSwan Color");
@@ -178,59 +178,36 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping("Watara Supervision", NewznabStandardCategory.ConsoleOther, "Watara Supervision"); caps.Categories.AddCategoryMapping("Watara Supervision", NewznabStandardCategory.ConsoleOther, "Watara Supervision");
caps.Categories.AddCategoryMapping("Retro - Other", NewznabStandardCategory.ConsoleOther, "Retro - Other"); caps.Categories.AddCategoryMapping("Retro - Other", NewznabStandardCategory.ConsoleOther, "Retro - Other");
// special categories (real categories/not platforms)
caps.Categories.AddCategoryMapping("OST", NewznabStandardCategory.AudioOther, "OST"); caps.Categories.AddCategoryMapping("OST", NewznabStandardCategory.AudioOther, "OST");
caps.Categories.AddCategoryMapping("Applications", NewznabStandardCategory.PC0day, "Applications"); caps.Categories.AddCategoryMapping("Applications", NewznabStandardCategory.PC0day, "Applications");
caps.Categories.AddCategoryMapping("E-Books", NewznabStandardCategory.BooksEBook, "E-Books"); caps.Categories.AddCategoryMapping("E-Books", NewznabStandardCategory.BooksEBook, "E-Books");
return caps; return caps;
} }
protected override async Task Test(List<ValidationFailure> failures)
{
((GazelleGamesRequestGenerator)GetRequestGenerator()).FetchPasskey();
await base.Test(failures);
}
} }
public class GazelleGamesRequestGenerator : IIndexerRequestGenerator public class GazelleGamesRequestGenerator : IIndexerRequestGenerator
{ {
public GazelleGamesSettings Settings { get; set; } public GazelleGamesSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; } public IndexerCapabilities Capabilities { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public GazelleGamesRequestGenerator() public GazelleGamesRequestGenerator()
{ {
} }
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
var searchString = term;
var searchType = Settings.SearchGroupNames ? "groupname" : "searchstr";
var queryCollection = new NameValueCollection
{
{ searchType, searchString },
{ "order_by", "time" },
{ "order_way", "desc" },
{ "action", "basic" },
{ "searchsubmit", "1" }
};
var i = 0;
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
{
queryCollection.Add($"artistcheck[{i++}]", cat);
}
searchUrl += "?" + queryCollection.GetQueryString();
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories)));
return pageableRequests; return pageableRequests;
} }
@@ -239,7 +216,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories)));
return pageableRequests; return pageableRequests;
} }
@@ -248,7 +225,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories)); pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories)));
return pageableRequests; return pageableRequests;
} }
@@ -257,7 +234,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories)));
return pageableRequests; return pageableRequests;
} }
@@ -266,11 +243,67 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories)));
return pageableRequests; return pageableRequests;
} }
public void FetchPasskey()
{
// GET on index for the passkey
var request = RequestBuilder().Resource("api.php?request=quick_user").Build();
var indexResponse = HttpClient.Execute(request);
var index = Json.Deserialize<GazelleGamesUserResponse>(indexResponse.Content);
if (index == null ||
string.IsNullOrWhiteSpace(index.Status) ||
index.Status != "success" ||
string.IsNullOrWhiteSpace(index.Response.PassKey))
{
throw new Exception("Failed to authenticate with GazelleGames.");
}
// Set passkey on settings so it can be used to generate the download URL
Settings.Passkey = index.Response.PassKey;
}
private IEnumerable<IndexerRequest> GetRequest(string parameters)
{
var req = RequestBuilder()
.Resource($"api.php?{parameters}")
.Build();
yield return new IndexerRequest(req);
}
private HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
.Accept(HttpAccept.Json)
.SetHeader("X-API-Key", Settings.Apikey);
}
private string GetBasicSearchParameters(string searchTerm, int[] categories)
{
var parameters = "request=search&search_type=torrents&empty_groups=filled&order_by=time&order_way=desc";
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var searchType = Settings.SearchGroupNames ? "groupname" : "searchstr";
parameters += string.Format("&{1}={0}", searchTerm.Replace(".", " "), searchType);
}
if (categories != null)
{
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
{
parameters += string.Format("&artistcheck[]={0}", cat);
}
}
return parameters;
}
public Func<IDictionary<string, string>> GetCookies { get; set; } public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
} }
@@ -290,117 +323,183 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
var torrentInfos = new List<ReleaseInfo>(); var torrentInfos = new List<ReleaseInfo>();
var rowsSelector = ".torrent_table > tbody > tr"; if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
var searchResultParser = new HtmlParser();
var searchResultDocument = searchResultParser.ParseDocument(indexerResponse.Content);
var rows = searchResultDocument.QuerySelectorAll(rowsSelector);
var stickyGroup = false;
string categoryStr;
ICollection<IndexerCategory> groupCategory = null;
string groupTitle = null;
foreach (var row in rows)
{ {
if (row.ClassList.Contains("torrent")) throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
var jsonResponse = new HttpResponse<GazelleGamesResponse>(indexerResponse.HttpResponse);
if (jsonResponse.Resource.Status != "success" ||
string.IsNullOrWhiteSpace(jsonResponse.Resource.Status) ||
jsonResponse.Resource.Response == null)
{
return torrentInfos;
}
foreach (var result in jsonResponse.Resource.Response)
{
Dictionary<string, GazelleGamesTorrent> torrents;
try
{
torrents = ((JObject)result.Value.Torrents).ToObject<Dictionary<string, GazelleGamesTorrent>>();
}
catch
{ {
// garbage rows
continue; continue;
} }
else if (row.ClassList.Contains("group"))
if (result.Value.Torrents != null)
{ {
stickyGroup = row.ClassList.Contains("sticky"); var categories = result.Value.Artists.Select(a => a.Name);
var dispalyname = row.QuerySelector("#displayname");
var qCat = row.QuerySelector("td.cats_col > div"); foreach (var torrent in torrents)
categoryStr = qCat.GetAttribute("title");
var qArtistLink = dispalyname.QuerySelector("#groupplatform > a");
if (qArtistLink != null)
{ {
categoryStr = ParseUtil.GetArgumentFromQueryString(qArtistLink.GetAttribute("href"), "artistname"); var id = int.Parse(torrent.Key);
var infoUrl = GetInfoUrl(result.Key, id);
var release = new TorrentInfo()
{
Guid = infoUrl,
Title = torrent.Value.ReleaseTitle,
Files = torrent.Value.FileCount,
Grabs = torrent.Value.Snatched,
Size = long.Parse(torrent.Value.Size),
DownloadUrl = GetDownloadUrl(id),
InfoUrl = infoUrl,
Seeders = torrent.Value.Seeders,
Categories = _categories.MapTrackerCatDescToNewznab(categories.FirstOrDefault()),
Peers = torrent.Value.Leechers + torrent.Value.Seeders,
PublishDate = torrent.Value.Time.ToUniversalTime(),
DownloadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.FreeLeech || torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1,
UploadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1
};
torrentInfos.Add(release);
} }
groupCategory = _categories.MapTrackerCatToNewznab(categoryStr);
var qDetailsLink = dispalyname.QuerySelector("#groupname > a");
groupTitle = qDetailsLink.TextContent;
}
else if (row.ClassList.Contains("group_torrent"))
{
if (row.QuerySelector("td.edition_info") != null)
{
continue;
}
var sizeString = row.QuerySelector("td:nth-child(4)").TextContent;
if (string.IsNullOrEmpty(sizeString))
{
continue;
}
var qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]");
var title = qDetailsLink.TextContent.Replace(", Freeleech!", "").Replace(", Neutral Leech!", "");
//if (stickyGroup && (query.ImdbID == null || !NewznabStandardCategory.MovieSearchImdbAvailable) && !query.MatchQueryStringAND(title)) // AND match for sticky releases
//{
// continue;
//}
var qDescription = qDetailsLink.QuerySelector("span.torrent_info_tags");
var qDLLink = row.QuerySelector("a[href^=\"torrents.php?action=download\"]");
var qTime = row.QuerySelector("span.time");
var qGrabs = row.QuerySelector("td:nth-child(5)");
var qSeeders = row.QuerySelector("td:nth-child(6)");
var qLeechers = row.QuerySelector("td:nth-child(7)");
var qFreeLeech = row.QuerySelector("strong.freeleech_label");
var qNeutralLeech = row.QuerySelector("strong.neutralleech_label");
var time = qTime.GetAttribute("title");
var link = _settings.BaseUrl + qDLLink.GetAttribute("href");
var seeders = ParseUtil.CoerceInt(qSeeders.TextContent);
var publishDate = DateTime.SpecifyKind(
DateTime.ParseExact(time, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture),
DateTimeKind.Unspecified).ToLocalTime();
var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href");
var grabs = ParseUtil.CoerceInt(qGrabs.TextContent);
var leechers = ParseUtil.CoerceInt(qLeechers.TextContent);
var size = ParseUtil.GetBytes(sizeString);
var release = new TorrentInfo
{
MinimumRatio = 1,
MinimumSeedTime = 288000, //80 hours
Categories = groupCategory,
PublishDate = publishDate,
Size = size,
InfoUrl = details,
DownloadUrl = link,
Guid = link,
Grabs = grabs,
Seeders = seeders,
Peers = leechers + seeders,
Title = title,
Description = qDescription?.TextContent,
UploadVolumeFactor = qNeutralLeech is null ? 1 : 0,
DownloadVolumeFactor = qFreeLeech != null || qNeutralLeech != null ? 0 : 1
};
torrentInfos.Add(release);
} }
} }
return torrentInfos.ToArray(); // order by date
return
torrentInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
private string GetDownloadUrl(int torrentId)
{
// AuthKey is required but not checked, just pass in a dummy variable
// to avoid having to track authkey, which is randomly cycled
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId)
.AddQueryParam("authkey", "prowlarr")
.AddQueryParam("torrent_pass", _settings.Passkey);
return url.FullUri;
}
private string GetInfoUrl(string groupId, int torrentId)
{
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("id", groupId)
.AddQueryParam("torrentid", torrentId);
return url.FullUri;
} }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
} }
public class GazelleGamesSettings : CookieTorrentBaseSettings public class GazelleGamesSettingsValidator : AbstractValidator<GazelleGamesSettings>
{ {
public GazelleGamesSettingsValidator()
{
RuleFor(c => c.Apikey).NotEmpty();
}
}
public class GazelleGamesSettings : NoAuthTorrentBaseSettings
{
private static readonly GazelleGamesSettingsValidator Validator = new GazelleGamesSettingsValidator();
public GazelleGamesSettings() public GazelleGamesSettings()
{ {
SearchGroupNames = false; Apikey = "";
Passkey = "";
} }
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings), Must have User Permissions", Privacy = PrivacyLevel.ApiKey)]
public string Apikey { get; set; }
[FieldDefinition(3, Label = "Search Group Names", Type = FieldType.Checkbox, HelpText = "Search Group Names Only")] [FieldDefinition(3, Label = "Search Group Names", Type = FieldType.Checkbox, HelpText = "Search Group Names Only")]
public bool SearchGroupNames { get; set; } public bool SearchGroupNames { get; set; }
public string Passkey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public class GazelleGamesResponse
{
public string Status { get; set; }
public Dictionary<string, GazelleGamesGroup> Response { get; set; }
}
public class GazelleGamesGroup
{
public List<GazelleGamesArtist> Artists { get; set; }
public object Torrents { get; set; }
}
public class GazelleGamesArtist
{
public string Id { get; set; }
public string Name { get; set; }
}
public class GazelleGamesTorrent
{
public string Size { get; set; }
public int? Snatched { get; set; }
public int Seeders { get; set; }
public int Leechers { get; set; }
public string ReleaseTitle { get; set; }
public DateTime Time { get; set; }
public int FileCount { get; set; }
public GazelleGamesFreeTorrent FreeTorrent { get; set; }
public bool PersonalFL { get; set; }
public bool LowSeedFL { get; set; }
}
public class GazelleGamesUserResponse
{
public string Status { get; set; }
public GazelleGamesUser Response { get; set; }
}
public class GazelleGamesUser
{
public string PassKey { get; set; }
}
public enum GazelleGamesFreeTorrent
{
Normal,
FreeLeech,
Neutral,
Either
} }
} }

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Headphones
var request = requestBuilder.Build(); var request = requestBuilder.Build();
request.AddBasicAuthentication(Settings.Username, Settings.Password); request.Credentials = new BasicNetworkCredential(Settings.Username, Settings.Password);
try try
{ {

View File

@@ -131,7 +131,7 @@ namespace NzbDrone.Core.Indexers.Headphones
} }
var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss); var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
request.HttpRequest.AddBasicAuthentication(Settings.Username, Settings.Password); request.HttpRequest.Credentials = new BasicNetworkCredential(Settings.Username, Settings.Password);
yield return request; yield return request;
} }

View File

@@ -101,29 +101,12 @@ namespace NzbDrone.Core.Indexers.Rarbg
{ {
Settings.Validate().Filter("BaseUrl").ThrowOnError(); Settings.Validate().Filter("BaseUrl").ThrowOnError();
try var request = new HttpRequestBuilder(Settings.BaseUrl.Trim('/'))
{
var request = new HttpRequestBuilder(Settings.BaseUrl.Trim('/'))
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}") .Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
.Accept(HttpAccept.Json) .Accept(HttpAccept.Json)
.Build(); .Build();
_httpClient.Get(request); _httpClient.Get(request);
}
catch (CloudFlareCaptchaException ex)
{
return new
{
captchaRequest = new
{
host = ex.CaptchaRequest.Host,
ray = ex.CaptchaRequest.Ray,
siteKey = ex.CaptchaRequest.SiteKey,
secretToken = ex.CaptchaRequest.SecretToken,
responseUrl = ex.CaptchaRequest.ResponseUrl.FullUri,
}
};
}
return new return new
{ {

View File

@@ -26,12 +26,6 @@ namespace NzbDrone.Core.Indexers.Rarbg
.Resource("/pubapi_v2.php") .Resource("/pubapi_v2.php")
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
if (Settings.CaptchaToken.IsNotNullOrWhiteSpace())
{
requestBuilder.UseSimplifiedUserAgent = true;
requestBuilder.SetCookie("cf_clearance", Settings.CaptchaToken);
}
requestBuilder.AddQueryParam("mode", "search"); requestBuilder.AddQueryParam("mode", "search");
if (imdbId.IsNotNullOrWhiteSpace()) if (imdbId.IsNotNullOrWhiteSpace())

View File

@@ -14,8 +14,5 @@ namespace NzbDrone.Core.Indexers.Rarbg
[FieldDefinition(2, Type = FieldType.Checkbox, Label = "Ranked Only", HelpText = "Only include ranked results.")] [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Ranked Only", HelpText = "Only include ranked results.")]
public bool RankedOnly { get; set; } public bool RankedOnly { get; set; }
[FieldDefinition(3, Type = FieldType.Captcha, Label = "CAPTCHA Token", HelpText = "CAPTCHA Clearance token used to handle CloudFlare Anti-DDOS measures on shared-ip VPNs.")]
public string CaptchaToken { get; set; }
} }
} }

View File

@@ -36,12 +36,6 @@ namespace NzbDrone.Core.Indexers.Rarbg
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}") .Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
if (settings.CaptchaToken.IsNotNullOrWhiteSpace())
{
requestBuilder.UseSimplifiedUserAgent = true;
requestBuilder.SetCookie("cf_clearance", settings.CaptchaToken);
}
var response = _httpClient.Get<JObject>(requestBuilder.Build()); var response = _httpClient.Get<JObject>(requestBuilder.Build());
return response.Resource["token"].ToString(); return response.Resource["token"].ToString();

View File

@@ -154,7 +154,7 @@ namespace NzbDrone.Core.Indexers.Definitions
throw new ReleaseUnavailableException("Downloading torrent failed", ex); throw new ReleaseUnavailableException("Downloading torrent failed", ex);
} }
if ((int)ex.Response.StatusCode == 429) if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
_logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri); _logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri);
} }

View File

@@ -61,6 +61,12 @@ namespace NzbDrone.Core.Indexers.Torznab
torrentInfo.ImdbId = int.Parse(GetImdbId(item).Substring(2)); torrentInfo.ImdbId = int.Parse(GetImdbId(item).Substring(2));
} }
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
torrentInfo.DownloadVolumeFactor = downloadFactor;
torrentInfo.UploadVolumeFactor = uploadFactor;
torrentInfo.IndexerFlags = GetFlags(item); torrentInfo.IndexerFlags = GetFlags(item);
torrentInfo.PosterUrl = GetPosterUrl(item); torrentInfo.PosterUrl = GetPosterUrl(item);
} }

View File

@@ -232,7 +232,7 @@ namespace NzbDrone.Core.Indexers
_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); _indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
} }
_logger.Warn("API Request Limit reached for {0}", this); _logger.Warn("Request Limit reached for {0}", this);
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -251,19 +251,12 @@ namespace NzbDrone.Core.Indexers
_indexerStatusService.RecordFailure(Definition.Id); _indexerStatusService.RecordFailure(Definition.Id);
_logger.Warn("Invalid Credentials for {0} {1}", this, url); _logger.Warn("Invalid Credentials for {0} {1}", this, url);
} }
catch (CloudFlareCaptchaException ex) catch (CloudFlareProtectionException ex)
{ {
result.Queries.Add(new IndexerQueryResult { Response = ex.Response }); result.Queries.Add(new IndexerQueryResult { Response = ex.Response });
_indexerStatusService.RecordFailure(Definition.Id); _indexerStatusService.RecordFailure(Definition.Id);
ex.WithData("FeedUrl", url); ex.WithData("FeedUrl", url);
if (ex.IsExpired) _logger.Error(ex, "Cloudflare protection detected for {0}, Flaresolverr may be required.", this);
{
_logger.Error(ex, "Expired CAPTCHA token for {0}, please refresh in indexer settings.", this);
}
else
{
_logger.Error(ex, "CAPTCHA token required for {0}, check indexer settings.", this);
}
} }
catch (IndexerException ex) catch (IndexerException ex)
{ {
@@ -399,13 +392,18 @@ namespace NzbDrone.Core.Indexers
{ {
_logger.Warn("HTTP Error - {0}", response); _logger.Warn("HTTP Error - {0}", response);
if ((int)response.StatusCode == 429) if (response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(request.HttpRequest, response); throw new TooManyRequestsException(request.HttpRequest, response);
} }
} }
UpdateCookies(Cookies, DateTime.Now + TimeSpan.FromDays(30)); if (CloudFlareDetectionService.IsCloudflareProtected(response))
{
throw new CloudFlareProtectionException(response);
}
UpdateCookies(request.HttpRequest.Cookies, DateTime.Now + TimeSpan.FromDays(30));
return new IndexerResponse(request, response); return new IndexerResponse(request, response);
} }
@@ -471,16 +469,9 @@ namespace NzbDrone.Core.Indexers
{ {
_logger.Warn("Request limit reached: " + ex.Message); _logger.Warn("Request limit reached: " + ex.Message);
} }
catch (CloudFlareCaptchaException ex) catch (CloudFlareProtectionException ex)
{ {
if (ex.IsExpired) return new ValidationFailure(string.Empty, ex.Message);
{
return new ValidationFailure("CaptchaToken", "CloudFlare CAPTCHA token expired, please Refresh.");
}
else
{
return new ValidationFailure("CaptchaToken", "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required.");
}
} }
catch (UnsupportedFeedException ex) catch (UnsupportedFeedException ex)
{ {

View File

@@ -15,7 +15,10 @@ namespace NzbDrone.Core.Indexers
RId, RId,
TvMazeId, TvMazeId,
TraktId, TraktId,
TmdbId TmdbId,
DoubanId,
Genre,
Year
} }
public enum MovieSearchParam public enum MovieSearchParam
@@ -26,7 +29,9 @@ namespace NzbDrone.Core.Indexers
ImdbTitle, ImdbTitle,
ImdbYear, ImdbYear,
TraktId, TraktId,
Genre Genre,
DoubanId,
Year
} }
public enum MusicSearchParam public enum MusicSearchParam
@@ -36,7 +41,8 @@ namespace NzbDrone.Core.Indexers
Artist, Artist,
Label, Label,
Year, Year,
Genre Genre,
Track
} }
public enum SearchParam public enum SearchParam
@@ -48,7 +54,10 @@ namespace NzbDrone.Core.Indexers
{ {
Q, Q,
Title, Title,
Author Author,
Publisher,
Genre,
Year
} }
public class IndexerCapabilities public class IndexerCapabilities
@@ -71,6 +80,9 @@ namespace NzbDrone.Core.Indexers
public bool TvSearchTvMazeAvailable => TvSearchParams.Contains(TvSearchParam.TvMazeId); public bool TvSearchTvMazeAvailable => TvSearchParams.Contains(TvSearchParam.TvMazeId);
public bool TvSearchTraktAvailable => TvSearchParams.Contains(TvSearchParam.TraktId); public bool TvSearchTraktAvailable => TvSearchParams.Contains(TvSearchParam.TraktId);
public bool TvSearchTmdbAvailable => TvSearchParams.Contains(TvSearchParam.TmdbId); public bool TvSearchTmdbAvailable => TvSearchParams.Contains(TvSearchParam.TmdbId);
public bool TvSearchDoubanAvailable => TvSearchParams.Contains(TvSearchParam.DoubanId);
public bool TvSearchGenreAvailable => TvSearchParams.Contains(TvSearchParam.Genre);
public bool TvSearchYearAvailable => TvSearchParams.Contains(TvSearchParam.Year);
public List<MovieSearchParam> MovieSearchParams; public List<MovieSearchParam> MovieSearchParams;
public bool MovieSearchAvailable => MovieSearchParams.Count > 0; public bool MovieSearchAvailable => MovieSearchParams.Count > 0;
@@ -78,12 +90,15 @@ namespace NzbDrone.Core.Indexers
public bool MovieSearchTmdbAvailable => MovieSearchParams.Contains(MovieSearchParam.TmdbId); public bool MovieSearchTmdbAvailable => MovieSearchParams.Contains(MovieSearchParam.TmdbId);
public bool MovieSearchTraktAvailable => MovieSearchParams.Contains(MovieSearchParam.TraktId); public bool MovieSearchTraktAvailable => MovieSearchParams.Contains(MovieSearchParam.TraktId);
public bool MovieSearchGenreAvailable => MovieSearchParams.Contains(MovieSearchParam.Genre); public bool MovieSearchGenreAvailable => MovieSearchParams.Contains(MovieSearchParam.Genre);
public bool MovieSearchYearAvailable => MovieSearchParams.Contains(MovieSearchParam.Year);
public bool MovieSearchDoubanAvailable => MovieSearchParams.Contains(MovieSearchParam.DoubanId);
public List<MusicSearchParam> MusicSearchParams; public List<MusicSearchParam> MusicSearchParams;
public bool MusicSearchAvailable => MusicSearchParams.Count > 0; public bool MusicSearchAvailable => MusicSearchParams.Count > 0;
public bool MusicSearchAlbumAvailable => MusicSearchParams.Contains(MusicSearchParam.Album); public bool MusicSearchAlbumAvailable => MusicSearchParams.Contains(MusicSearchParam.Album);
public bool MusicSearchArtistAvailable => MusicSearchParams.Contains(MusicSearchParam.Artist); public bool MusicSearchArtistAvailable => MusicSearchParams.Contains(MusicSearchParam.Artist);
public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label); public bool MusicSearchLabelAvailable => MusicSearchParams.Contains(MusicSearchParam.Label);
public bool MusicSearchTrackAvailable => MusicSearchParams.Contains(MusicSearchParam.Track);
public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year); public bool MusicSearchYearAvailable => MusicSearchParams.Contains(MusicSearchParam.Year);
public bool MusicSearchGenreAvailable => MusicSearchParams.Contains(MusicSearchParam.Genre); public bool MusicSearchGenreAvailable => MusicSearchParams.Contains(MusicSearchParam.Genre);
@@ -91,6 +106,9 @@ namespace NzbDrone.Core.Indexers
public bool BookSearchAvailable => BookSearchParams.Count > 0; public bool BookSearchAvailable => BookSearchParams.Count > 0;
public bool BookSearchTitleAvailable => BookSearchParams.Contains(BookSearchParam.Title); public bool BookSearchTitleAvailable => BookSearchParams.Contains(BookSearchParam.Title);
public bool BookSearchAuthorAvailable => BookSearchParams.Contains(BookSearchParam.Author); public bool BookSearchAuthorAvailable => BookSearchParams.Contains(BookSearchParam.Author);
public bool BookSearchPublisherAvailable => BookSearchParams.Contains(BookSearchParam.Publisher);
public bool BookSearchYearAvailable => BookSearchParams.Contains(BookSearchParam.Year);
public bool BookSearchGenreAvailable => BookSearchParams.Contains(BookSearchParam.Genre);
public readonly IndexerCapabilitiesCategories Categories; public readonly IndexerCapabilitiesCategories Categories;
public List<IndexerFlag> Flags; public List<IndexerFlag> Flags;
@@ -302,6 +320,21 @@ namespace NzbDrone.Core.Indexers
parameters.Add("tmdbid"); parameters.Add("tmdbid");
} }
if (TvSearchDoubanAvailable)
{
parameters.Add("doubanid");
}
if (TvSearchGenreAvailable)
{
parameters.Add("genre");
}
if (TvSearchYearAvailable)
{
parameters.Add("year");
}
return string.Join(",", parameters); return string.Join(",", parameters);
} }
@@ -335,6 +368,16 @@ namespace NzbDrone.Core.Indexers
parameters.Add("genre"); parameters.Add("genre");
} }
if (MovieSearchDoubanAvailable)
{
parameters.Add("doubanid");
}
if (MovieSearchYearAvailable)
{
parameters.Add("year");
}
return string.Join(",", parameters); return string.Join(",", parameters);
} }
@@ -356,6 +399,11 @@ namespace NzbDrone.Core.Indexers
parameters.Add("label"); parameters.Add("label");
} }
if (MusicSearchTrackAvailable)
{
parameters.Add("track");
}
if (MusicSearchYearAvailable) if (MusicSearchYearAvailable)
{ {
parameters.Add("year"); parameters.Add("year");
@@ -382,6 +430,21 @@ namespace NzbDrone.Core.Indexers
parameters.Add("author"); parameters.Add("author");
} }
if (BookSearchPublisherAvailable)
{
parameters.Add("publisher");
}
if (BookSearchGenreAvailable)
{
parameters.Add("genre");
}
if (BookSearchYearAvailable)
{
parameters.Add("year");
}
return string.Join(",", parameters); return string.Join(",", parameters);
} }

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