Compare commits

...

119 Commits

Author SHA1 Message Date
Qstick
a0d18c546e Bump version to 0.4.9 2022-11-12 20:03:35 -06:00
Qstick
d935b0df82 Fix regression in release analytics service after debounce added
Fixes #1193
2022-11-10 17:39:14 -06:00
Qstick
9e37f69224 Fixed: (RetroFlix) Urls built with double slash
Fixes #1188
Closes #1192
2022-11-10 06:54:20 -06:00
Servarr
2805c4f18b Automated API Docs update 2022-11-07 20:22:57 -06:00
Qstick
dae21f22b9 Bump version to 0.4.8 2022-11-07 19:30:14 -06:00
Qstick
7ddbe09eca New: Base API info endpoint 2022-11-07 19:26:54 -06:00
bakerboy448
90e3c809c3 New: Notifiarr moved from webhook to API
New: Notifiarr Add Instance Name Support

Fixed: Notifiarr - Better HTTP Error Handling

also quiet sentry

move apikey to header from url
(cherry picked from commit 1db690ad39ec103c0f4dc89ac4545801ef95bec7)

Fixed: Improve Notifiarr Exception Handling and Validation Errors

(cherry picked from commit 6aaa024d71b939030950460ae986ada5bbae5ad7)
2022-11-07 19:23:16 -06:00
Weblate
ec8cf5f57a Translated using Weblate (Finnish)
Currently translated at 99.5% (462 of 464 strings)

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translation: Servarr/Prowlarr
2022-11-07 19:22:26 -06:00
Bakerboy448
f4bbf2f8af Fixed: (Avistaz) Handle 429 Request Limit Reached 2022-11-04 09:55:11 -05:00
bakerboy448
ea98d41472 Update feature_request.yml
[skip ci]
2022-11-04 09:54:04 -05:00
bakerboy448
b8cb0fd291 update bug report template [skip ci] 2022-11-04 09:54:04 -05:00
Bakerboy448
d3dfa620ac Fix confusing session expired test message 2022-11-04 09:53:27 -05:00
Weblate
049668f307 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (464 of 464 strings)

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

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 87.0% (404 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.7% (364 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: korax1970 <duxlatronum@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
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/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-11-04 09:48:54 -05:00
Yukine
c400575aac Fixed: (AnimeBytes) add delimiter to episode release 2022-11-04 09:48:14 -05:00
Yukine
6f122fb2e4 New: (AnimeBytes) add filename support for single episodes 2022-11-04 09:47:52 -05:00
Qstick
a9c210f8e7 Create CODE_OF_CONDUCT.md 2022-11-03 15:58:57 -05:00
ta264
1068ba8915 Use wildcard pattern now we have better bsd agent 2022-11-03 11:10:41 +00:00
ta264
635335d876 Revert "Temp disable BSD Tests"
This reverts commit 438ea380f5.
2022-11-03 11:10:41 +00:00
Qstick
2ed51cd933 Fixed: Nullref on Cardigann without login test 2022-10-30 18:06:19 -05:00
Qstick
b74c46c554 Ignore brotli test on osx 2022-10-30 13:25:34 -05:00
Qstick
7029e0d6ee Enable new Servarr build notifications 2022-10-25 21:59:08 -05:00
Qstick
438ea380f5 Temp disable BSD Tests 2022-10-25 21:54:19 -05:00
Qstick
4eec675d61 Fix Baker Problems 2022-10-25 21:51:48 -05:00
bakerboy448
0a9bd8287f New: Return 429 for Query and Grab Limits 2022-10-25 21:11:13 -05:00
Qstick
b583ac3a97 Fixed: (Cardigann) Rework login required logic
Fixes #1166
2022-10-25 20:21:27 -05:00
bakerboy448
4be41ff3fb fixup! 2022-10-18 10:49:12 -05:00
bakerboy448
b911f8cc08 Fix: (RetroFlix) Update URL to .club
Fixes #1159
2022-10-18 10:49:12 -05:00
Weblate
22face385f Translated using Weblate (Portuguese)
Currently translated at 78.7% (364 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: korax1970 <duxlatronum@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-10-14 22:22:40 -05:00
Qstick
3e700b63c2 New: Retry Postgres connection 3 times (with 5 second sleep) on Startup 2022-10-10 22:30:00 -05:00
Qstick
df0b8fc660 And another..... 2022-10-09 19:04:30 -05:00
Qstick
f96dbbfc21 Ensure FS doesn't fail when no proxy 2022-10-09 19:03:53 -05:00
Qstick
4a75f92cb5 Fixed: (FlareSolverr) Send non-auth global proxy when set
Fixes #1142
2022-10-09 18:33:41 -05:00
Qstick
dd05a9dbd4 Obsolete Anthelion C# Indexer 2022-10-09 10:34:40 -05:00
Qstick
e78b8d5346 New: Add long term Application status Healthcheck 2022-10-09 10:15:50 -05:00
Qstick
74a1d95ab7 Update NZBIndex Categories 2022-10-08 22:52:17 -05:00
Qstick
f929a7e62f New: (Indexer) NZBIndex 2022-10-08 22:14:46 -05:00
Qstick
e9e4248af4 New: (Indexer) RetroFlix 2022-10-08 19:18:12 -05:00
Yukine
9e3b43ef12 Fixed: (GreatPosterWall) correctly override Gazelle base method 2022-10-08 18:38:46 -05:00
Qstick
738a690aac Fixed: (Rarbg) Incorrect TVDB param logic
Fixes #1129

Co-Authored-By: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2022-10-08 18:20:41 -05:00
Qstick
3b7c59e9bb Fixed: (Rarbg) More reliable token handling and retry
Fixes #1148
2022-10-08 18:11:22 -05:00
Qstick
b8ca28d955 Fixed: Explicitly forbid redirects on Gazelle search requests
Fixes #1144
2022-10-08 15:31:51 -05:00
Yukine
8797bb7d1c Remove unused Gazelle legacy code 2022-10-04 06:46:20 -05:00
Yukine
be430732f5 Fixed: (GreatPosterWall) move imdb id search to searchstr query param 2022-10-04 06:46:20 -05:00
h96kikh6
e7b1380b85 Fixed: (Indexer) HDSpace - Added new categories
Added new categories: 45 - HDTV 2160 -> Prowlarr: Movies/UHD (2045), 46 - Movies 2160 -> Prowlarr: TV/UHD (5045), 47 - Doc 2160 -> Prowlarr: TV/Documentary (5080), 48 - Animation 2160 -> Prowlarr: TV/Anime (5070), 49 - XXX 2160 -> Prowlarr: XXX/UHD (6045)
2022-10-02 11:08:51 -05:00
Qstick
c29735741c Optimize Indexer updates (v2) 2022-09-29 22:47:00 -05:00
Qstick
f56a13a375 Bump Mailkit to 3.4.1 2022-09-29 22:14:25 -05:00
Qstick
148d8ee249 Bump Sentry to 3.21.0 2022-09-29 22:14:25 -05:00
Qstick
3547028b96 Bump YamlDotNet to 12.0.1 2022-09-29 22:14:25 -05:00
Qstick
e4ffa1873e Fixed: Definition not updating if local file is missing 2022-09-29 22:13:21 -05:00
Qstick
2e85a21576 Fixed: (GazelleGames) Serialization error on empty response
Fixes #1137
2022-09-29 20:14:18 -05:00
Qstick
0a111e7572 Fixed: (Cardigann) Search path redirect
Fixes #1102
2022-09-26 21:13:57 -05:00
Qstick
25217c0ee8 Fixed: TypeError on Keyup in Firefox for IndexerIndex 2022-09-25 20:44:26 -05:00
Qstick
791592927c Purge old PTP Radarr check 2022-09-25 12:13:49 -05:00
Qstick
4137193a60 Fixed: (Avistaz) FL Only should be checkbox 2022-09-25 10:08:12 -05:00
Qstick
99816bfd36 Fix test error due to DryIOC update 2022-09-24 20:24:17 -05:00
Qstick
59e5b5bd52 Set PooledConnectionLifetime to 10 minutes
Setting PooledConnectinLifetime to a defined number will ensure we don't run into DNS refresh issues
2022-09-18 21:45:10 -05:00
Qstick
7fa0a2b33c Bump Swashbuckle to 6.4.0 2022-09-18 21:43:06 -05:00
Qstick
0593ca6b9e Bump DryIoc to 5.2.2 2022-09-18 21:40:32 -05:00
Qstick
06a26b5c87 Fixed: (RarBG) Don't disable indexer on temp rate limit
Fixes #1027
2022-09-18 15:56:31 -05:00
Weblate
dcae6dc151 Translated using Weblate (Slovak)
Currently translated at 12.5% (58 of 462 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.3% (362 of 462 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.3% (362 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (462 of 462 strings)

Added translation using Weblate (Latvian)

Co-authored-by: Dainel Amendoeira <daniel@amendoeira.eu>
Co-authored-by: Gylesie <github-anon.dasheens@aleeas.com>
Co-authored-by: HiNesslio <chi.lio@shms-mail.ch>
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/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-09-18 15:54:43 -05:00
Qstick
04e3ed0ffe Fixed: (Gazelle) Download fails if out of FL tokens
Fixes #1088
2022-09-18 15:39:09 -05:00
Qstick
1ed5ed9179 Bump version to 0.4.7 2022-09-18 14:55:58 -05:00
Qstick
d292d086ee Prevent query failures on Cardigann Indexers 2022-09-12 22:49:28 -05:00
Qstick
f68915c5dd New: Don't query indexers if they don't support query categories 2022-09-12 22:28:07 -05:00
Qstick
01e970e1a7 New: (Avistaz) Genre Search Support
Fixes #1097
2022-09-12 21:27:32 -05:00
Qstick
68df439498 New: (Avistaz) Freeleech Only Setting
#1108
2022-09-12 21:16:05 -05:00
Qstick
33de7ca7ab Fixed: (MoreThanTv) Parsing issue when download url is null
Fixes #1047
2022-09-10 12:50:32 -05:00
Qstick
ae2d9b795b Don't reset request Url when calculating RedirectUrl 2022-09-10 12:24:41 -05:00
Qstick
eadea745f8 Warn on redirect to alt domain when checking if login required 2022-09-10 12:24:41 -05:00
Qstick
f958c4aefa Bump version to 0.4.6 2022-09-08 20:57:06 -05:00
Weblate
4cf9fb0e79 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (462 of 462 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (461 of 462 strings)

Translated using Weblate (French)

Currently translated at 95.6% (442 of 462 strings)

Translated using Weblate (French)

Currently translated at 95.2% (440 of 462 strings)

Translated using Weblate (German)

Currently translated at 99.1% (458 of 462 strings)

Translated using Weblate (French)

Currently translated at 95.2% (440 of 462 strings)

Translated using Weblate (French)

Currently translated at 94.8% (438 of 462 strings)

Translated using Weblate (Spanish)

Currently translated at 77.7% (359 of 462 strings)

Co-authored-by: Fradri <adrien.riotte@live.fr>
Co-authored-by: Gian Klug <gian.klug@ict-scouts.ch>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mijail Todorovich <mijailtodorovich+git@gmail.com>
Co-authored-by: Thomas Schwery <thomas@schwery.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: berrre <zdch6W75@gmail.com>
Co-authored-by: cikyw <cikyw@vomoto.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-09-08 20:29:59 -05:00
psylenced
bfa68347e6 Fix: Trace logging postgres cleanse for large json files. 2022-09-08 20:28:05 -05:00
Qstick
f97b35403d Fixed: Indexer proxies not applying to requests
Fixes #1107
2022-09-05 19:55:46 -05:00
Weblate
bf2e057247 Translated using Weblate (Russian)
Currently translated at 76.1% (352 of 462 strings)

Translated using Weblate (Romanian)

Currently translated at 71.8% (332 of 462 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: emanuelsipos <emanuelsipos1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translation: Servarr/Prowlarr
2022-08-18 23:12:04 -05:00
Qstick
5a278f4e9d Fixed: Set default null value for Genre, Publisher, Douban parameters 2022-08-18 22:30:04 -05:00
Yukine
232a6efd0d New: (Indexer) GreatPosterWall (#1085)
* New: (Indexer) GreatPosterWall

* fix(GreatPosterWall): time is Chinese and not UTC

* feat(GreatPosterWall): add canUseToken check to GetDownloadUrl

* feat(GreatPosterWall): add fileName property as title
2022-08-18 20:04:29 -05:00
Chris
7e01c93b2c Fixed: Regex in log cleanser taking 10+ minutes on messages longer than 100k.
(cherry picked from commit d01bae92bfd68b04c54ab19bafe8af16c68ce711)
2022-08-18 20:03:31 -05:00
Qstick
d58f6551e6 Fixed: Set Units for Seed Time settings 2022-08-18 19:56:13 -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
184 changed files with 57461 additions and 3189 deletions

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@prowlarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -27,10 +27,7 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
## 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)
[![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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order;
if (event.path.length === 4) {
if (event.composedPath && event.composedPath().length === 4) {
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] });
}

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import UISettings from './UISettings';
@@ -11,18 +12,19 @@ const SECTION = 'ui';
function createLanguagesSelector() {
return createSelector(
(state) => state.settings.languages,
(languages) => {
const items = languages.items;
const filterItems = ['Any', 'Unknown'];
(state) => state.localization,
(localization) => {
console.log(localization);
const items = localization.items;
if (!items) {
return [];
}
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => {
const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
return {
key: item.id,
key: item.value,
value: item.name
};
});
@@ -51,6 +53,7 @@ const mapDispatchToProps = {
setUISettingsValue,
saveUISettings,
fetchUISettings,
fetchLocalizationOptions,
clearPendingChanges
};
@@ -61,6 +64,7 @@ class UISettingsConnector extends Component {
componentDidMount() {
this.props.fetchUISettings();
this.props.fetchLocalizationOptions();
}
componentWillUnmount() {
@@ -96,6 +100,7 @@ UISettingsConnector.propTypes = {
setUISettingsValue: PropTypes.func.isRequired,
saveUISettings: PropTypes.func.isRequired,
fetchUISettings: PropTypes.func.isRequired,
fetchLocalizationOptions: 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',
label: 'Indexer',
label: translate('Indexer'),
isSortable: false,
isVisible: true
},
{
name: 'query',
label: 'Query',
label: translate('Query'),
isSortable: false,
isVisible: true
},
{
name: 'parameters',
label: 'Parameters',
label: translate('Parameters'),
isSortable: false,
isVisible: false
},
{
name: 'grabTitle',
label: 'Grab Title',
label: translate('Grab Title'),
isSortable: false,
isVisible: false
},
{
name: 'categories',
label: 'Categories',
label: translate('Categories'),
isSortable: false,
isVisible: true
},
@@ -72,13 +72,13 @@ export const defaultState = {
},
{
name: 'source',
label: 'Source',
label: translate('Source'),
isSortable: false,
isVisible: false
},
{
name: 'elapsedTime',
label: 'Elapsed Time',
label: translate('Elapsed Time'),
isSortable: false,
isVisible: true
},

View File

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

View File

@@ -36,7 +36,7 @@ export const defaultState = {
columns: [
{
name: 'select',
columnLabel: 'Select',
columnLabel: translate('Select'),
isSortable: false,
isVisible: true,
isModifiable: false,
@@ -51,7 +51,7 @@ export const defaultState = {
},
{
name: 'sortName',
label: 'Indexer Name',
label: translate('IndexerName'),
isSortable: true,
isVisible: true,
isModifiable: false
@@ -88,7 +88,7 @@ export const defaultState = {
},
{
name: 'capabilities',
label: 'Categories',
label: translate('Categories'),
isSortable: false,
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 indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
@@ -16,7 +15,6 @@ export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications';
export * from './Settings/applications';
export * from './Settings/appProfiles';
@@ -38,7 +36,6 @@ export const defaultState = {
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState,
applications: applications.defaultState,
appProfiles: appProfiles.defaultState,
@@ -68,7 +65,6 @@ export const actionHandlers = handleThunks({
...general.actionHandlers,
...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers,
...applications.actionHandlers,
...appProfiles.actionHandlers,
@@ -89,7 +85,6 @@ export const reducers = createHandleActions({
...general.reducers,
...indexerCategories.reducers,
...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers,
...applications.reducers,
...appProfiles.reducers,

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using FluentAssertions;
using Moq;
@@ -15,8 +16,11 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http
{
@@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost;
private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp]
public void FixtureSetUp()
{
@@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com";
// 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.
_httpBinHost = mainHost;
@@ -46,31 +52,22 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10;
_httpBinSleep = 10;
}
private bool IsTestSiteAvailable(string site)
{
try
{
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest;
var res = req.GetResponse() as HttpWebResponse;
var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
if (res.StatusCode != HttpStatusCode.OK)
{
return false;
}
try
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
if (res == null || res.StatusCode != (HttpStatusCode)429)
if (res == null || res.StatusCode != HttpStatusCode.TooManyRequests)
{
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.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http
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]
public void should_execute_typed_get()
{
@@ -162,15 +184,45 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message);
}
[TestCase("gzip")]
public void should_execute_get_using_gzip(string compression)
[Test]
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);
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.Brotli.Should().BeFalse();
}
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
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)]
@@ -190,6 +242,28 @@ namespace NzbDrone.Common.Test.Http
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]
public void should_not_follow_redirects_when_not_in_production()
{
@@ -315,13 +389,38 @@ namespace NzbDrone.Common.Test.Http
{
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();
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]
public void should_send_cookie()
{
@@ -753,6 +852,7 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; }
public string Data { get; set; }
public bool Gzipped { get; set; }
public bool Brotli { get; set; }
}
public class HttpCookieResource

View File

@@ -98,15 +98,30 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
}
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
[TestCase(@"Auth-Success ip 32.2.3.5")]

View File

@@ -210,5 +210,26 @@ namespace NzbDrone.Common.Extensions
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
// NOTE: we are not checking non-ascii characters and we should
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[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -24,7 +25,7 @@ namespace NzbDrone.Common.Http
var matches = _CookieRegex.Match(cookieHeader);
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;
}

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
{
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
Task DownloadFileAsync(string url, string fileName);
}
}

View File

@@ -1,13 +1,16 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
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 NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -15,221 +18,225 @@ namespace NzbDrone.Common.Http.Dispatchers
{
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 ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
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;
_createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_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)
{
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.
// we should just stick with the more compatible Gzip
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
if (cookieHeader.IsNotNullOrWhiteSpace())
{
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)
{
AddRequestHeaders(webRequest, request.Headers);
AddRequestHeaders(requestMessage, request.Headers);
}
HttpWebResponse httpWebResponse;
var httpClient = GetClient(request.Url, request.ProxySettings);
var sw = new Stopwatch();
sw.Start();
try
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{
if (request.ContentData != null)
{
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
byte[] data = null;
httpWebResponse = (HttpWebResponse)await webRequest.GetResponseAsync();
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
try
{
// The default messages for WebException on mono are pretty horrible.
if (e.Status == WebExceptionStatus.NameResolutionFailure)
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
}
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);
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
}
else
{
throw;
data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
}
}
}
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
catch (Exception ex)
{
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();
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
try
{
cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
}
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)
{
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;
return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
}
}
protected virtual IWebProxy GetProxy(HttpUri uri)
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri, HttpProxySettings requestProxy)
{
IWebProxy proxy = null;
var proxySettings = requestProxy ?? _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,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
}
};
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)
{
switch (header.Key)
{
case "Accept":
webRequest.Accept = header.Value;
webRequest.Headers.Accept.ParseAdd(header.Value);
break;
case "Connection":
webRequest.Connection = header.Value;
webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
break;
case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value);
AddContentHeader(webRequest, "Content-Length", header.Value);
break;
case "Content-Type":
webRequest.ContentType = header.Value;
AddContentHeader(webRequest, "Content-Type", header.Value);
break;
case "Date":
webRequest.Date = HttpHeader.ParseDateTime(header.Value);
webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
break;
case "Expect":
webRequest.Expect = header.Value;
webRequest.Headers.Expect.ParseAdd(header.Value);
break;
case "Host":
webRequest.Host = header.Value;
webRequest.Headers.Host = header.Value;
break;
case "If-Modified-Since":
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break;
case "Range":
throw new NotImplementedException();
case "Referer":
webRequest.Referer = header.Value;
webRequest.Headers.Add("Referer", header.Value);
break;
case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value;
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
break;
case "User-Agent":
webRequest.UserAgent = header.Value;
webRequest.Headers.UserAgent.Clear();
webRequest.Headers.UserAgent.ParseAdd(header.Value);
break;
case "Proxy-Connection":
throw new NotImplementedException();
@@ -239,5 +246,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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
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
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod)
if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
{
request.Method = HttpMethod.Get;
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);
}
@@ -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"]);
}
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);
}
@@ -124,6 +136,21 @@ namespace NzbDrone.Common.Http
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)
{
foreach (var interceptor in _requestInterceptors)
@@ -140,8 +167,6 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, 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.
/*lock (_cookieContainerCache)
var sourceContainer = new CookieContainer();
var responseCookies = response.GetCookies();
if (responseCookies.Count != 0)
{
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
foreach (var pair in responseCookies)
{
Cookie cookie;
if (pair.Value == null)
{
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
sourceContainer.Add((Uri)request.Url, cookie);
}
}
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();
if (cookieHeaders.Empty())
{
return;
}
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie)
{
lock (_cookieContainerCache)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
foreach (var cookieHeader in cookieHeaders)
{
try
{
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
}
}
AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
}
}
}
private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
{
foreach (var cookieHeader in cookieHeaders)
{
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)
{
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)

View File

@@ -4,11 +4,27 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using NzbDrone.Common.Extensions;
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 HttpHeader(NameValueCollection headers)
@@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http
{
}
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
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
{
get

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
namespace NzbDrone.Common.Http
{
@@ -12,11 +14,13 @@ namespace NzbDrone.Common.Http
{
public HttpRequest(string url, HttpAccept httpAccept = null)
{
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
Method = HttpMethod.Get;
ConnectionKeepAlive = true;
AllowAutoRedirect = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction)
@@ -34,19 +38,23 @@ namespace NzbDrone.Common.Http
public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; }
public HttpProxySettings ProxySettings { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString()
{
@@ -103,12 +111,5 @@ namespace NzbDrone.Common.Http
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 HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { 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 bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
@@ -46,6 +47,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>();
LogHttpError = true;
}
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -106,6 +108,7 @@ namespace NzbDrone.Common.Http
request.Method = Method;
request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;
request.StoreRequestCookie = StoreRequestCookie;
@@ -113,13 +116,7 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent;
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);
}
request.Credentials = NetworkCredential;
foreach (var header in Headers)
{
@@ -212,7 +209,7 @@ namespace NzbDrone.Common.Http
}
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
{
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 body = Encoding.UTF8.GetBytes(urlencoded);
var body = Encoding.GetBytes(urlencoded);
request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(body);
@@ -406,7 +403,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData
{
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;

View File

@@ -64,10 +64,11 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.MultipleChoices ||
StatusCode == HttpStatusCode.PermanentRedirect ||
Headers.ContainsKey("Refresh");
public string RedirectUrl
@@ -88,13 +89,13 @@ namespace NzbDrone.Common.Http
if (match.Success)
{
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri;
return (Request.Url + new HttpUri(match.Groups[2].Value)).FullUri;
}
return string.Empty;
}
return (Request.Url += new HttpUri(newUrl)).FullUri;
return (Request.Url + new HttpUri(newUrl)).FullUri;
}
}
@@ -117,7 +118,7 @@ namespace NzbDrone.Common.Http
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))
{

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

@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -4,14 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<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="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.18.0" />
<PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

View File

@@ -0,0 +1,4 @@
{
"status": "success",
"response": []
}

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.Http;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework
@@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
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<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
}

View File

@@ -60,6 +60,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.ImdbId.Should().Be(15569106);
torrentInfo.TmdbId.Should().Be(135144);
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,83 @@
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;
using NzbDrone.Test.Common;
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);
}
[Test]
public async Task should_not_error_if_empty_response()
{
var recentFeed = ReadAllText(@"Files/Indexers/GazelleGames/recentfeed-empty.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(0);
}
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
};
Mocker.GetMock<IRarbgTokenProvider>()
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), It.IsAny<string>()))
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
.Returns("validtoken");
}

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 NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@@ -16,7 +15,7 @@ namespace NzbDrone.Core.Test.Localization
[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);
}

View File

@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -6,7 +6,6 @@ using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Security;
@@ -139,9 +138,9 @@ namespace NzbDrone.Core.Configuration
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); }
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Configuration
string TimeFormat { get; set; }
bool ShowRelativeDates { get; set; }
bool EnableColorImpairedMode { get; set; }
int UILanguage { get; set; }
string UILanguage { get; set; }
//Internal
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

@@ -1,6 +1,7 @@
using System;
using System.Data.Common;
using System.Data.SQLite;
using System.Net.Sockets;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
@@ -125,6 +126,37 @@ namespace NzbDrone.Core.Datastore
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
}
catch (NpgsqlException e)
{
if (e.InnerException is SocketException)
{
var retryCount = 3;
while (true)
{
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
try
{
_migrationController.Migrate(connectionString, migrationContext);
}
catch (Exception ex)
{
if (--retryCount > 0)
{
System.Threading.Thread.Sleep(5000);
continue;
}
throw new ProwlarrStartupException(ex, "Error creating main database");
}
}
}
else
{
throw new ProwlarrStartupException(e, "Error creating main database");
}
}
catch (Exception e)
{
throw new ProwlarrStartupException(e, "Error creating main database");

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.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Parser.Model;
@@ -113,8 +112,6 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<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<ReleaseInfo>());
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
{
public class Aria2Version
public class Aria2Fault
{
[XmlRpcMember("version")]
public string Version;
public Aria2Fault(XElement element)
{
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 string[] EnabledFeatures;
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
public class Aria2Uri
public class Aria2Version
{
[XmlRpcMember("status")]
public string Status;
public Aria2Version(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
if (e.ElementAsString("name") == "version")
{
Version = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("uri")]
public string Uri;
public string Version { get; set; }
}
public class Aria2File
{
[XmlRpcMember("index")]
public string Index;
public Aria2File(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("length")]
public string Length;
if (name == "path")
{
Path = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("completedLength")]
public string CompletedLength;
public string Path { get; set; }
}
[XmlRpcMember("path")]
public string Path;
public class Aria2Dict
{
public Aria2Dict(XElement element)
{
Dict = new Dictionary<string, string>();
[XmlRpcMember("selected")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Selected;
foreach (var e in element.XPathSelectElements("./struct/member"))
{
Dict.Add(e.ElementAsString("name"), e.Element("value").GetStringValue());
}
}
[XmlRpcMember("uris")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2Uri[] Uris;
public Dictionary<string, string> Dict { get; set; }
}
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
{
[XmlRpcMember("bittorrent")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public XmlRpcStruct Bittorrent;
public Aria2Status(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("bitfield")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Bitfield;
if (name == "bittorrent")
{
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")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string InfoHash;
[XmlRpcMember("completedLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string CompletedLength;
[XmlRpcMember("connections")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
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;
public Aria2Bittorrent Bittorrent { get; set; }
public string InfoHash { get; set; }
public string CompletedLength { get; set; }
public string DownloadSpeed { get; set; }
public Aria2File[] Files { get; set; }
public string Gid { get; set; }
public string Status { get; set; }
public string TotalLength { get; set; }
public string UploadLength { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using CookComputing.XmlRpc;
using NLog;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2
{
@@ -12,46 +12,103 @@ namespace NzbDrone.Core.Download.Clients.Aria2
string GetVersion(Aria2Settings settings);
string AddUri(Aria2Settings settings, string magnet);
string AddTorrent(Aria2Settings settings, byte[] torrent);
Dictionary<string, string> GetGlobals(Aria2Settings settings);
List<Aria2Status> GetTorrents(Aria2Settings settings);
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
{
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)
@@ -59,86 +116,29 @@ namespace NzbDrone.Core.Download.Clients.Aria2
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}";
}
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
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
{
return task();
}
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);
}
LogResponseContent = true,
};
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");
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
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");
var httpRequest = requestBuilder.Build();

View File

@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
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)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
};
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)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}

View File

@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
.Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.AllowAutoRedirect = false;
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.Collections.Generic;
using System.Linq;
using System.Net;
using CookComputing.XmlRpc;
using NLog;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
@@ -18,122 +20,70 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, 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();
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
}
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)
{
_logger.Debug("Executing remote method: system.client_version");
var document = ExecuteRequest(settings, "system.client_version");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion());
return version;
return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
}
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 ret = ExecuteRequest(() => client.TorrentMulticall(
"",
"",
"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
var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
?.Elements()
.Select(x => new RTorrentTorrent(x))
.ToList()
?? new List<RTorrentTorrent>();
_logger.Trace(ret.ToJson());
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;
return torrents;
}
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
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));
}
});
var args = new List<object> { "", torrentUrl };
args.AddRange(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);
}
@@ -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)
{
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
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));
}
});
var args = new List<object> { "", fileContent };
args.AddRange(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);
}
@@ -164,25 +113,29 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
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);
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
if (response.GetStringResponse() != 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)
{
_logger.Debug("Executing remote method: d.erase");
var response = ExecuteRequest(settings, "d.erase", hash);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0)
if (response.GetIntResponse() != 0)
{
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)
{
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try
{
var name = ExecuteRequest(() => client.GetName(hash));
var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
if (name.IsNullOrWhiteSpace())
{
@@ -235,45 +185,34 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray();
}
private IRTorrent BuildClient(RTorrentSettings settings)
private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
{
var client = XmlRpcProxyGen.Create<IRTorrent>();
client.Url = string.Format(@"{0}://{1}:{2}/{3}",
settings.UseSsl ? "https" : "http",
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
LogResponseContent = true,
};
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)
{
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);
}
var response = _httpClient.Execute(request);
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 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 Hash { get; set; }
public string Path { get; set; }
@@ -10,6 +38,7 @@
public long RemainingSize { get; set; }
public long DownRate { get; set; }
public long Ratio { get; set; }
public long FinishedTime { get; set; }
public bool IsFinished { get; set; }
public bool IsOpen { get; set; }
public bool IsActive { get; set; }

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
.Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
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

@@ -0,0 +1,59 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Applications;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IApplication>))]
[CheckOn(typeof(ProviderDeletedEvent<IApplication>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IApplication>))]
public class ApplicationLongTermStatusCheck : HealthCheckBase
{
private readonly IApplicationFactory _providerFactory;
private readonly IApplicationStatusService _providerStatusService;
public ApplicationLongTermStatusCheck(IApplicationFactory providerFactory,
IApplicationStatusService providerStatusService,
ILocalizationService localizationService)
: base(localizationService)
{
_providerFactory = providerFactory;
_providerStatusService = providerStatusService;
}
public override HealthCheck Check()
{
var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id,
s => s.ProviderId,
(i, s) => new { Provider = i, Status = s })
.Where(p => p.Status.InitialFailure.HasValue &&
p.Status.InitialFailure.Value.Before(
DateTime.UtcNow.AddHours(-6)))
.ToList();
if (backOffProviders.Empty())
{
return new HealthCheck(GetType());
}
if (backOffProviders.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ApplicationLongTermStatusCheckAllClientMessage"),
"#applications-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("ApplicationLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#applications-are-unavailable-due-to-failures");
}
}
}

View File

@@ -1,34 +0,0 @@
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.PassThePopcorn;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class PTPOldSettingsCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;
public PTPOldSettingsCheck(IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerFactory = indexerFactory;
}
public override HealthCheck Check()
{
var ptpIndexers = _indexerFactory.All().Where(i => i.Settings.GetType() == typeof(PassThePopcornSettings));
var ptpIndexerOldSettings = ptpIndexers
.Where(i => (i.Settings as PassThePopcornSettings).APIUser.IsNullOrWhiteSpace()).Select(i => i.Name);
if (ptpIndexerOldSettings.Any())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("PtpOldSettingsCheckMessage"), string.Join(", ", ptpIndexerOldSettings)));
}
return new HealthCheck(GetType());
}
}
}

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

@@ -10,7 +10,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Validation;
@@ -18,12 +20,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
{
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx" };
private readonly ICached<string> _cache;
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
public FlareSolverr(IHttpProxySettingsProvider proxySettingsProvider, IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
: base(cloudRequestBuilder, httpClient, logger, localizationService)
{
_proxySettingsProvider = proxySettingsProvider;
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
}
@@ -45,7 +48,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public override HttpResponse PostResponse(HttpResponse response)
{
if (!IsCloudflareProtected(response))
if (!CloudFlareDetectionService.IsCloudflareProtected(response))
{
_logger.Debug("CF Protection not detected, returning original response");
return response;
@@ -53,14 +56,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
var flaresolverrResponse = _httpClient.Execute(GenerateFlareSolverrRequest(response.Request));
FlareSolverrResponse result = null;
if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError)
{
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;
@@ -70,26 +71,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
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);
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)
{
var rCookies = flareSolverrResponse.Solution.Cookies;
@@ -116,6 +103,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
var maxTimeout = Settings.RequestTimeout * 1000;
// Use Proxy if no credentials are set (creds not supported as of FS 2.2.9)
var proxySettings = _proxySettingsProvider.GetProxySettings();
var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null;
if (request.Method == HttpMethod.Get)
{
req = new FlareSolverrRequestGet
@@ -123,14 +114,18 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
Cmd = "request.get",
Url = url,
MaxTimeout = maxTimeout,
UserAgent = userAgent
UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
};
}
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 postData = request.GetContent();
@@ -146,10 +141,15 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
ContentLength = null
},
MaxTimeout = maxTimeout,
UserAgent = userAgent
UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
};
}
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
throw new FlareSolverrException("Unimplemented POST Content-Type: " + request.Headers.ContentType);
@@ -169,9 +169,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
newRequest.Headers.ContentType = "application/json";
newRequest.Method = HttpMethod.Post;
newRequest.LogResponseContent = true;
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;
}
@@ -205,38 +206,59 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
return new ValidationResult(failures);
}
public class FlareSolverrRequest
private Uri GetProxyUri(HttpProxySettings proxySettings)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks4:
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks5:
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
default:
return null;
}
}
private class FlareSolverrRequest
{
public string Cmd { get; set; }
public string Url { get; set; }
public string UserAgent { get; set; }
public Cookie[] Cookies { get; set; }
public FlareSolverrProxy Proxy { get; set; }
}
public class FlareSolverrRequestGet : FlareSolverrRequest
private class FlareSolverrRequestGet : FlareSolverrRequest
{
public string Headers { get; set; }
public int MaxTimeout { get; set; }
}
public class FlareSolverrRequestPost : FlareSolverrRequest
private class FlareSolverrRequestPost : FlareSolverrRequest
{
public string PostData { get; set; }
public int MaxTimeout { get; set; }
}
public class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
private class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
{
public HeadersPost Headers { get; set; }
}
public class HeadersPost
private class FlareSolverrProxy
{
public string Url { get; set; }
}
private class HeadersPost
{
public string ContentType { get; set; }
public string ContentLength { get; set; }
}
public class FlareSolverrResponse
private class FlareSolverrResponse
{
public string Status { get; set; }
public string Message { get; set; }
@@ -246,7 +268,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public Solution Solution { get; set; }
}
public class Solution
private class Solution
{
public string Url { get; set; }
public string Status { get; set; }
@@ -256,7 +278,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public string UserAgent { get; set; }
}
public class Cookie
private class Cookie
{
public string Name { get; set; }
public string Value { get; set; }
@@ -273,7 +295,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value);
}
public class Headers
private class Headers
{
public string Status { get; set; }
public string Date { get; set; }

View File

@@ -3,7 +3,9 @@ using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Notifications.Prowl;
namespace NzbDrone.Core.IndexerProxies.Http
{
@@ -18,14 +20,13 @@ namespace NzbDrone.Core.IndexerProxies.Http
public override HttpRequest PreRequest(HttpRequest request)
{
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null, new NetworkCredential(Settings.Username, Settings.Password));
}
else
{
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null);
}
request.ProxySettings = new HttpProxySettings(ProxyType.Http,
Settings.Host,
Settings.Port,
null,
false,
Settings.Username,
Settings.Password);
_logger.Debug("Applying HTTP(S) Proxy {0} to request {1}", Name, request.Url);

View File

@@ -4,6 +4,7 @@ using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.IndexerProxies.Socks4
@@ -25,14 +26,13 @@ namespace NzbDrone.Core.IndexerProxies.Socks4
return null;
}
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new WebProxy(uri, false, null, new NetworkCredential(Settings.Username, Settings.Password));
}
else
{
request.Proxy = new WebProxy(uri);
}
request.ProxySettings = new HttpProxySettings(ProxyType.Socks4,
Settings.Host,
Settings.Port,
null,
false,
Settings.Username,
Settings.Password);
_logger.Debug("Applying Socks4 Proxy {0} to request {1}", Name, request.Url);

View File

@@ -4,6 +4,7 @@ using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.IndexerProxies.Socks5
@@ -26,14 +27,13 @@ namespace NzbDrone.Core.IndexerProxies.Socks5
return null;
}
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace())
{
request.Proxy = new WebProxy(uri, false, null, new NetworkCredential(Settings.Username, Settings.Password));
}
else
{
request.Proxy = new WebProxy(uri);
}
request.ProxySettings = new HttpProxySettings(ProxyType.Socks5,
Settings.Host,
Settings.Port,
null,
false,
Settings.Username,
Settings.Password);
_logger.Debug("Applying Socks5 Proxy {0} to request {1}", Name, request.Url);

View File

@@ -6,6 +6,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{
public string Author { 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
{

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string ImdbId { get; set; }
public int? TmdbId { get; set; }
public int? TraktId { get; set; }
public int? DoubanId { get; set; }
public int? Year { 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 Label { get; set; }
public string Genre { get; set; }
public string Track { get; set; }
public int? Year { get; set; }
public override bool RssSearch

View File

@@ -17,6 +17,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? TvMazeId { get; set; }
public int? TraktId { 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 EpisodeSearchString => GetEpisodeSearchString();

View File

@@ -4,10 +4,10 @@ namespace NzbDrone.Core.IndexerSearch
{
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 MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?: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 BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:title\:)(?<title>[^{]+))\}", 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>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", 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>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string t { get; set; }
public string q { get; set; }
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.IndexerSearch
public int? tvmazeid { get; set; }
public int? traktid { get; set; }
public int? tvdbid { get; set; }
public int? doubanid { get; set; }
public int? season { get; set; }
public string ep { get; set; }
public string album { get; set; }
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.IndexerSearch
public string genre { get; set; }
public string author { get; set; }
public string title { get; set; }
public string publisher { get; set; }
public string configured { get; set; }
public string source { 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;
}
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)
{
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;
}
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["imdbid"].Success)
{
imdbid = match.Groups["imdbid"].Value;
@@ -104,6 +121,11 @@ namespace NzbDrone.Core.IndexerSearch
album = match.Groups["album"].Value;
}
if (match.Groups["track"].Success)
{
track = match.Groups["track"].Value;
}
if (match.Groups["label"].Success)
{
label = match.Groups["label"].Value;
@@ -129,6 +151,11 @@ namespace NzbDrone.Core.IndexerSearch
title = match.Groups["title"].Value;
}
if (match.Groups["publisher"].Success)
{
publisher = match.Groups["publisher"].Value;
}
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")),
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c.Id, protocol),
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol),
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, protocol),
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol),
@@ -107,6 +107,8 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), 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("minimumratio", t.MinimumRatio, protocol),
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),

View File

@@ -1,13 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Analytics;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.IndexerSearch
{
@@ -16,26 +20,48 @@ namespace NzbDrone.Core.IndexerSearch
private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IAnalyticsService _analyticsService;
private readonly Debouncer _debouncer;
private readonly Logger _logger;
private readonly List<ReleaseInfo> _pendingUpdates;
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
{
_debouncer = new Debouncer(SendReleases, TimeSpan.FromMinutes(10));
_analyticsService = analyticsService;
_requestBuilder = requestBuilder.Releases;
_httpClient = httpClient;
_logger = logger;
_pendingUpdates = new List<ReleaseInfo>();
}
public void HandleAsync(IndexerQueryEvent message)
{
if (_analyticsService.IsEnabled && 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();
request.Method = HttpMethod.Post;
request.Headers.ContentType = "application/json";
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,
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.TmdbId = request.tmdbid;
searchSpec.TraktId = request.traktid;
searchSpec.DoubanId = request.doubanid;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
@@ -73,6 +74,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Album = request.album;
searchSpec.Label = request.label;
searchSpec.Genre = request.genre;
searchSpec.Track = request.track;
searchSpec.Year = request.year;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
@@ -88,8 +90,11 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.ImdbId = request.imdbid;
searchSpec.TraktId = request.traktid;
searchSpec.TmdbId = request.tmdbid;
searchSpec.DoubanId = request.doubanid;
searchSpec.RId = request.rid;
searchSpec.TvMazeId = request.tvmazeid;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
}
@@ -100,6 +105,9 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Author = request.author;
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) };
}
@@ -152,6 +160,18 @@ namespace NzbDrone.Core.IndexerSearch
.ToList();
}
if (criteriaBase.Categories != null && criteriaBase.Categories.Length > 0)
{
//Only query supported indexers
indexers = indexers.Where(i => ((IndexerDefinition)i.Definition).Capabilities.Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList();
if (indexers.Count == 0)
{
_logger.Debug("All provided categories are unsupported by selected indexers: {0}", string.Join(", ", criteriaBase.Categories));
return new List<ReleaseInfo>();
}
}
_logger.ProgressInfo("Searching indexer(s): [{0}] for {1}", string.Join(", ", indexers.Select(i => i.Definition.Name).ToList()), criteriaBase.ToString());
var tasks = indexers.Select(x => DispatchIndexer(searchAction, x, criteriaBase));

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 6;
private const int DEFINITION_VERSION = 7;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()
@@ -282,44 +283,29 @@ namespace NzbDrone.Core.IndexerVersions
{
var startupFolder = _appFolderInfo.AppDataFolder;
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
var currentDefs = _versionService.All().ToDictionary(x => x.DefinitionId, x => x.Sha);
try
{
EnsureDefinitionsFolder();
foreach (var def in response.Resource)
var definitionsFolder = Path.Combine(startupFolder, "Definitions");
var saveFile = Path.Combine(definitionsFolder, $"indexers.zip");
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile);
using (ZipArchive archive = ZipFile.OpenRead(saveFile))
{
try
{
var saveFile = Path.Combine(startupFolder, "Definitions", $"{def.File}.yml");
if (currentDefs.TryGetValue(def.Id, out var defSha) && defSha == def.Sha)
{
_logger.Trace("Indexer already up to date: {0}", def.File);
continue;
}
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{def.File}", saveFile);
_versionService.Upsert(new IndexerDefinitionVersion { Sha = def.Sha, DefinitionId = def.Id, File = def.File, LastUpdated = DateTime.UtcNow });
_cache.Remove(def.File);
_logger.Debug("Updated definition: {0}", def.File);
}
catch (Exception ex)
{
_logger.Error("Definition download failed: {0}, {1}", def.File, ex.Message);
}
archive.ExtractToDirectory(definitionsFolder, true);
}
_diskProvider.DeleteFile(saveFile);
_cache.Clear();
_logger.Debug("Updated indexer definitions");
}
catch (Exception ex)
{
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder);
_logger.Error(ex, "Definition update failed");
}
}
}

View File

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

View File

@@ -321,9 +321,10 @@ namespace NzbDrone.Core.Indexers.Definitions
if (episode != null)
{
releaseInfo = episode is > 0 and < 10
var episodeString = episode is > 0 and < 10
? "0" + episode
: episode.ToString();
releaseInfo = $" - {episodeString}";
}
else
{
@@ -449,6 +450,37 @@ namespace NzbDrone.Core.Indexers.Definitions
// Additional 5 hours per GB
minimumSeedTime += (int)((size / 1000000000) * 18000);
if (_settings.UseFilenameForSingleEpisodes && torrent.FileCount == 1)
{
var fileName = torrent.Files.First().FileName;
var guid = new Uri(details + "&nh=" + StringUtil.Hash(fileName));
var release = new TorrentInfo
{
MinimumRatio = 1,
MinimumSeedTime = minimumSeedTime,
Title = fileName,
InfoUrl = details.AbsoluteUri,
Guid = guid.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
PublishDate = publishDate,
Categories = category,
Description = description,
Size = size,
Seeders = seeders,
Peers = peers,
Grabs = snatched,
Files = fileCount,
DownloadVolumeFactor = rawDownMultiplier,
UploadVolumeFactor = rawUpMultiplier,
};
torrentInfos.Add(release);
continue;
}
foreach (var title in synonyms)
{
var releaseTitle = groupName == "Movie" ?
@@ -510,6 +542,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Passkey = "";
Username = "";
EnableSonarrCompatibility = true;
UseFilenameForSingleEpisodes = false;
}
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
@@ -521,6 +554,9 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(4, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
public bool EnableSonarrCompatibility { get; set; }
[FieldDefinition(5, Label = "Use Filenames for Single Episodes", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr replace AnimeBytes release names with the actual filename, this currently only works for single episode releases")]
public bool UseFilenameForSingleEpisodes { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
@@ -678,10 +714,22 @@ namespace NzbDrone.Core.Indexers.Definitions
[JsonProperty("FileCount")]
public int FileCount { get; set; }
[JsonProperty("FileList")]
public List<File> Files { get; set; }
[JsonProperty("UploadTime")]
public DateTimeOffset UploadTime { get; set; }
}
public class File
{
[JsonProperty("filename")]
public string FileName { get; set; }
[JsonProperty("size")]
public string FileSize { get; set; }
}
public class EditionData
{
[JsonProperty("EditionTitle")]

View File

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

View File

@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Moved to YML for Cardigann")]
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "Anthelion";

View File

@@ -36,11 +36,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvdbId
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvdbId, TvSearchParam.Genre
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.Genre
}
};

View File

@@ -40,6 +40,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[JsonProperty(PropertyName = "video_quality")]
public string VideoQuality { 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

View File

@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return torrentInfos.ToArray();
}
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
}
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
@@ -66,6 +71,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
UploadVolumeFactor = row.UploadMultiply,
MinimumRatio = 1,
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)

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
// hook to adjust the search category
protected virtual List<KeyValuePair<string, string>> GetBasicSearchParameters(int[] categories)
protected virtual List<KeyValuePair<string, string>> GetBasicSearchParameters(int[] categories, string genre)
{
var categoryMapping = Capabilities.Categories.MapTorznabCapsToTrackers(categories).Distinct().ToList();
var qc = new List<KeyValuePair<string, string>> // NameValueCollection don't support cat[]=19&cat[]=6
@@ -34,6 +34,16 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{ "type", categoryMapping.Any() ? categoryMapping.First() : "0" }
};
if (Settings.FreeleechOnly)
{
qc.Add("discount[]", "1");
}
if (genre.IsNotNullOrWhiteSpace())
{
qc.Add("tags", genre);
}
// resolution filter to improve the search
if (!categories.Contains(NewznabStandardCategory.Movies.Id) && !categories.Contains(NewznabStandardCategory.TV.Id) &&
!categories.Contains(NewznabStandardCategory.Audio.Id))
@@ -71,7 +81,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
@@ -93,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
@@ -104,7 +114,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
@@ -136,7 +146,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());

View File

@@ -22,6 +22,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public AvistazSettings()
{
Token = "";
FreeleechOnly = false;
}
public string Token { get; set; }
@@ -35,6 +36,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
public string Pid { get; set; }
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

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

View File

@@ -194,7 +194,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
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);
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories();
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 =
{

View File

@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.ContainsIgnoreCase("login.php"))
{
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
@@ -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));
}
}
var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row);
var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row);
if (skipRelease)
{
continue;
}
if (skipRelease)
{
continue;
}
releases.Add(release);
@@ -279,7 +279,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row);
var skipRelease = ParseRowFilters(filters, release, variables, row.ToHtmlPretty());
if (skipRelease)
{
@@ -594,8 +594,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.PosterUrl;
break;
case "genre":
release.Genres = release.Genres.Union(value.Split(',')).ToList();
value = release.Genres.ToString();
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
release.Genres = release.Genres.Union(value.Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries)).ToList();
value = string.Join(", ", release.Genres);
break;
case "year":
release.Year = ParseUtil.CoerceInt(value);
@@ -607,12 +608,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
case "booktitle":
release.BookTitle = value;
break;
case "publisher":
release.Publisher = value;
break;
case "artist":
release.Artist = value;
break;
case "album":
release.Album = value;
break;
case "label":
release.Label = value;
break;
case "track":
release.Track = value;
break;
default:
break;
}
@@ -644,6 +654,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
// for debugging
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString()));
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:
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
break;

View File

@@ -49,10 +49,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Movie"] = null;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
pageableRequests.Add(GetRequest(variables));
@@ -70,7 +72,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Album"] = searchCriteria.Album;
variables[".Query.Artist"] = searchCriteria.Artist;
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));
@@ -88,6 +92,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Series"] = null;
variables[".Query.Ep"] = searchCriteria.Episode;
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.IMDBIDShort"] = searchCriteria.ImdbId;
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.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
variables[".Query.Episode"] = searchCriteria.EpisodeSearchString;
pageableRequests.Add(GetRequest(variables));
@@ -112,6 +119,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Author"] = searchCriteria.Author;
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));
@@ -142,6 +152,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Offset"] = searchCriteria.Offset?.ToString() ?? null;
variables[".Query.Extended"] = null;
variables[".Query.APIKey"] = null;
variables[".Query.Genre"] = null;
//Movie
variables[".Query.Movie"] = null;
@@ -158,6 +169,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.TVRageID"] = null;
variables[".Query.TVMazeID"] = null;
variables[".Query.TraktID"] = null;
variables[".Query.DoubanID"] = null;
variables[".Query.Episode"] = null;
//Music
@@ -169,6 +181,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
//Book
variables[".Query.Author"] = null;
variables[".Query.Title"] = null;
variables[".Query.Publisher"] = null;
return variables;
}
@@ -343,6 +356,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.SetCookies(Cookies);
requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
@@ -930,6 +945,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
public bool CheckIfLoginIsNeeded(HttpResponse response)
{
if (_definition.Login == null)
{
return false;
}
if (response.HasHttpRedirect)
{
var domainHint = GetRedirectDomainHint(response);
@@ -937,35 +957,27 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var errormessage = "Got redirected to another domain. Try changing the indexer URL to " + domainHint + ".";
throw new CardigannException(errormessage);
_logger.Warn(errormessage);
}
return true;
}
if (_definition.Login == null || _definition.Login.Test == null)
{
return false;
}
if (response.HasHttpError)
{
return true;
}
// Only run html test selector on html responses
if (response.Headers.ContentType?.Contains("text/html") ?? true)
if (_definition.Login.Test?.Selector != null && (response.Headers.ContentType?.Contains("text/html") ?? true))
{
var parser = new HtmlParser();
var document = parser.ParseDocument(response.Content);
if (_definition.Login.Test.Selector != null)
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
if (selection.Length == 0)
{
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
if (selection.Length == 0)
{
return true;
}
return true;
}
}
@@ -1109,6 +1121,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
yield return request;
}
}

View File

@@ -36,11 +36,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.Genre
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.Genre
}
};

View File

@@ -80,6 +80,30 @@ namespace NzbDrone.Core.Indexers.Gazelle
_logger.Debug("Gazelle authentication succeeded.");
}
public override async Task<byte[]> Download(Uri link)
{
var response = await base.Download(link);
if (response.Length >= 1
&& response[0] != 'd' // simple test for torrent vs HTML content
&& link.Query.Contains("usetoken=1"))
{
var html = Encoding.GetString(response);
if (html.Contains("You do not have any freeleech tokens left.")
|| html.Contains("You do not have enough freeleech tokens")
|| html.Contains("This torrent is too large.")
|| html.Contains("You cannot use tokens here"))
{
// download again with usetoken=0
var requestLinkNew = link.ToString().Replace("usetoken=1", "usetoken=0");
response = await base.Download(new Uri(requestLinkNew));
}
}
return response;
}
protected override bool CheckIfLoginNeeded(HttpResponse response)
{
if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\"")))

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