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
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+132
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
-3
View File
@@ -27,10 +27,7 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
## Support ## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr) [![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
+87 -52
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.2' majorVersion: '0.4.9'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -97,15 +97,14 @@ stages:
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled" echo "Extra platforms already enabled"
else else
echo "Enabling BSD support" echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
displayName: Enable FreeBSD Support displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-bsd - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Prowlarr Backend displayName: Build Prowlarr Backend
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -119,24 +118,28 @@ stages:
displayName: Publish Backend displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests artifact: win-x64-tests
displayName: Publish Windows Test Package displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests artifact: linux-x64-tests
displayName: Publish Linux Test Package displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests artifact: linux-musl-x64-tests
displayName: Publish Linux Musl Test Package displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests artifact: freebsd-x64-tests
displayName: Publish FreeBSD Test Package displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests artifact: osx-x64-tests
displayName: Publish MacOS Test Package displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
@@ -239,35 +242,35 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd - bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Prowlarr" -exec chmod a+x {} \; find . -name "Prowlarr" -exec chmod a+x {} \;
find . -name "Prowlarr.Update" -exec chmod a+x {} \; find . -name "Prowlarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows Core zip displayName: Create win-x64 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows x86 Core zip displayName: Create win-x86 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core app displayName: Create osx-x64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar displayName: Create osx-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -275,14 +278,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app displayName: Create osx-arm64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar displayName: Create osx-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -290,7 +293,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Core tar displayName: Create linux-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -298,7 +301,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Musl Core tar displayName: Create linux-musl-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -306,7 +309,15 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -314,7 +325,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar displayName: Create linux-musl-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -322,7 +333,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar displayName: Create linux-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -330,7 +341,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar displayName: Create linux-musl-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -405,22 +416,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'FreebsdCore' testName: 'freebsd-x64'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -439,7 +450,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -469,8 +480,12 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -481,9 +496,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -513,14 +534,14 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost' Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432' Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr' Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
@@ -585,17 +606,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
@@ -612,7 +633,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -654,7 +675,7 @@ stages:
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
@@ -666,7 +687,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'LinuxCoreTests' artifactName: 'linux-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -720,14 +741,14 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'FreebsdCoreTests' artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '/$(pattern)' itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -756,11 +777,15 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'linux-musl-x64'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -770,9 +795,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -818,16 +849,19 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: true failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: true failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: true failBuild: true
@@ -845,7 +879,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(osName)CoreTests' artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -1074,4 +1108,5 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+32 -13
View File
@@ -25,15 +25,22 @@ UpdateVersionNumber()
fi fi
} }
EnableBsdSupport() EnableExtraPlatformsInSDK()
{ {
#todo enable sdk with SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -293,7 +300,8 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_BSD=NO ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -305,8 +313,12 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd) --enable-bsd|--enable-extra-platforms)
ENABLE_BSD=YES ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -350,12 +362,17 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
EnableBsdSupport EnableExtraPlatforms
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -365,9 +382,10 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -406,9 +424,10 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
+1 -14
View File
@@ -7,7 +7,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions'; import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -48,7 +48,6 @@ const selectIsPopulated = createSelector(
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
(state) => state.settings.general.isPopulated, (state) => state.settings.general.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.appProfiles.isPopulated, (state) => state.settings.appProfiles.isPopulated,
(state) => state.indexers.isPopulated, (state) => state.indexers.isPopulated,
(state) => state.indexerStatus.isPopulated, (state) => state.indexerStatus.isPopulated,
@@ -59,7 +58,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
generalSettingsIsPopulated, generalSettingsIsPopulated,
languagesIsPopulated,
appProfilesIsPopulated, appProfilesIsPopulated,
indexersIsPopulated, indexersIsPopulated,
indexerStatusIsPopulated, indexerStatusIsPopulated,
@@ -71,7 +69,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
generalSettingsIsPopulated && generalSettingsIsPopulated &&
languagesIsPopulated &&
appProfilesIsPopulated && appProfilesIsPopulated &&
indexersIsPopulated && indexersIsPopulated &&
indexerStatusIsPopulated && indexerStatusIsPopulated &&
@@ -86,7 +83,6 @@ const selectErrors = createSelector(
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
(state) => state.settings.general.error, (state) => state.settings.general.error,
(state) => state.settings.languages.error,
(state) => state.settings.appProfiles.error, (state) => state.settings.appProfiles.error,
(state) => state.indexers.error, (state) => state.indexers.error,
(state) => state.indexerStatus.error, (state) => state.indexerStatus.error,
@@ -97,7 +93,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -109,7 +104,6 @@ const selectErrors = createSelector(
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
generalSettingsError || generalSettingsError ||
languagesError ||
appProfilesError || appProfilesError ||
indexersError || indexersError ||
indexerStatusError || indexerStatusError ||
@@ -123,7 +117,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -166,9 +159,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchTags() { dispatchFetchTags() {
dispatch(fetchTags()); dispatch(fetchTags());
}, },
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexers() { dispatchFetchIndexers() {
dispatch(fetchIndexers()); dispatch(fetchIndexers());
}, },
@@ -216,7 +206,6 @@ class PageConnector extends Component {
if (!this.props.isPopulated) { if (!this.props.isPopulated) {
this.props.dispatchFetchCustomFilters(); this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchAppProfiles(); this.props.dispatchFetchAppProfiles();
this.props.dispatchFetchIndexers(); this.props.dispatchFetchIndexers();
this.props.dispatchFetchIndexerStatus(); this.props.dispatchFetchIndexerStatus();
@@ -242,7 +231,6 @@ class PageConnector extends Component {
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchTags, dispatchFetchTags,
dispatchFetchLanguages,
dispatchFetchAppProfiles, dispatchFetchAppProfiles,
dispatchFetchIndexers, dispatchFetchIndexers,
dispatchFetchIndexerStatus, dispatchFetchIndexerStatus,
@@ -283,7 +271,6 @@ PageConnector.propTypes = {
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired, dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchAppProfiles: PropTypes.func.isRequired, dispatchFetchAppProfiles: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired, dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchFetchIndexerStatus: PropTypes.func.isRequired, dispatchFetchIndexerStatus: PropTypes.func.isRequired,
@@ -20,9 +20,9 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.MOVIE_CONTINUING, iconName: icons.MOVIE_CONTINUING,
title: 'Indexers', title: translate('Indexers'),
to: '/', to: '/',
alias: '/movies', alias: '/indexers',
children: [ children: [
{ {
title: translate('Stats'), title: translate('Stats'),
@@ -33,13 +33,13 @@ const links = [
{ {
iconName: icons.SEARCH, iconName: icons.SEARCH,
title: 'Search', title: translate('Search'),
to: '/search' to: '/search'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: 'History', title: translate('History'),
to: '/history' to: '/history'
}, },
+1 -1
View File
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
onKeyUp = (event) => { onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order; 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) { if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] }); this.setState({ jumpToCharacter: jumpBarItems[0] });
} }
@@ -35,21 +35,21 @@ class IndexerIndexFooter extends PureComponent {
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.enabled} /> <div className={styles.enabled} />
<div> <div>
Enabled {translate('Enabled')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.redirected} /> <div className={styles.redirected} />
<div> <div>
Enabled, Redirected {translate('EnabledRedirected')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.disabled} /> <div className={styles.disabled} />
<div> <div>
Disabled {translate('Disabled')}
</div> </div>
</div> </div>
@@ -60,7 +60,7 @@ class IndexerIndexFooter extends PureComponent {
)} )}
/> />
<div> <div>
Error {translate('Error')}
</div> </div>
</div> </div>
</div> </div>
+1 -3
View File
@@ -62,8 +62,6 @@ class UISettings extends Component {
...otherProps ...otherProps
} = this.props; } = this.props;
const uiLanguages = languages.filter((item) => item.value !== 'Original');
const themeOptions = Object.keys(themes) const themeOptions = Object.keys(themes)
.map((theme) => ({ key: theme, value: titleCase(theme) })); .map((theme) => ({ key: theme, value: titleCase(theme) }));
@@ -172,7 +170,7 @@ class UISettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="uiLanguage" name="uiLanguage"
values={uiLanguages} values={languages}
helpText={translate('UILanguageHelpText')} helpText={translate('UILanguageHelpText')}
helpTextWarning={translate('UILanguageHelpTextWarning')} helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange} onChange={onInputChange}
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import UISettings from './UISettings'; import UISettings from './UISettings';
@@ -11,18 +12,19 @@ const SECTION = 'ui';
function createLanguagesSelector() { function createLanguagesSelector() {
return createSelector( return createSelector(
(state) => state.settings.languages, (state) => state.localization,
(languages) => { (localization) => {
const items = languages.items; console.log(localization);
const filterItems = ['Any', 'Unknown'];
const items = localization.items;
if (!items) { if (!items) {
return []; return [];
} }
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => { const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
return { return {
key: item.id, key: item.value,
value: item.name value: item.name
}; };
}); });
@@ -51,6 +53,7 @@ const mapDispatchToProps = {
setUISettingsValue, setUISettingsValue,
saveUISettings, saveUISettings,
fetchUISettings, fetchUISettings,
fetchLocalizationOptions,
clearPendingChanges clearPendingChanges
}; };
@@ -61,6 +64,7 @@ class UISettingsConnector extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchUISettings(); this.props.fetchUISettings();
this.props.fetchLocalizationOptions();
} }
componentWillUnmount() { componentWillUnmount() {
@@ -96,6 +100,7 @@ UISettingsConnector.propTypes = {
setUISettingsValue: PropTypes.func.isRequired, setUISettingsValue: PropTypes.func.isRequired,
saveUISettings: PropTypes.func.isRequired, saveUISettings: PropTypes.func.isRequired,
fetchUISettings: PropTypes.func.isRequired, fetchUISettings: PropTypes.func.isRequired,
fetchLocalizationOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };
@@ -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: {
}
};
+7 -7
View File
@@ -36,31 +36,31 @@ export const defaultState = {
}, },
{ {
name: 'indexer', name: 'indexer',
label: 'Indexer', label: translate('Indexer'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'query', name: 'query',
label: 'Query', label: translate('Query'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'parameters', name: 'parameters',
label: 'Parameters', label: translate('Parameters'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'grabTitle', name: 'grabTitle',
label: 'Grab Title', label: translate('Grab Title'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'categories', name: 'categories',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
@@ -72,13 +72,13 @@ export const defaultState = {
}, },
{ {
name: 'source', name: 'source',
label: 'Source', label: translate('Source'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'elapsedTime', name: 'elapsedTime',
label: 'Elapsed Time', label: translate('Elapsed Time'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
+2
View File
@@ -7,6 +7,7 @@ import * as indexers from './indexerActions';
import * as indexerIndex from './indexerIndexActions'; import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions'; import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions'; import * as indexerStatus from './indexerStatusActions';
import * as localization from './localizationActions';
import * as oAuth from './oAuthActions'; import * as oAuth from './oAuthActions';
import * as paths from './pathActions'; import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions'; import * as providerOptions from './providerOptionActions';
@@ -25,6 +26,7 @@ export default [
paths, paths,
providerOptions, providerOptions,
releases, releases,
localization,
indexers, indexers,
indexerIndex, indexerIndex,
indexerStats, indexerStats,
@@ -36,7 +36,7 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'select', name: 'select',
columnLabel: 'Select', columnLabel: translate('Select'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false, isModifiable: false,
@@ -51,7 +51,7 @@ export const defaultState = {
}, },
{ {
name: 'sortName', name: 'sortName',
label: 'Indexer Name', label: translate('IndexerName'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
@@ -88,7 +88,7 @@ export const defaultState = {
}, },
{ {
name: 'capabilities', name: 'capabilities',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
@@ -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);
@@ -8,7 +8,6 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories'; import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies'; import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
import ui from './Settings/ui'; import ui from './Settings/ui';
@@ -16,7 +15,6 @@ export * from './Settings/downloadClients';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/indexerCategories'; export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies'; export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications'; export * from './Settings/notifications';
export * from './Settings/applications'; export * from './Settings/applications';
export * from './Settings/appProfiles'; export * from './Settings/appProfiles';
@@ -38,7 +36,6 @@ export const defaultState = {
general: general.defaultState, general: general.defaultState,
indexerCategories: indexerCategories.defaultState, indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState, indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState, notifications: notifications.defaultState,
applications: applications.defaultState, applications: applications.defaultState,
appProfiles: appProfiles.defaultState, appProfiles: appProfiles.defaultState,
@@ -68,7 +65,6 @@ export const actionHandlers = handleThunks({
...general.actionHandlers, ...general.actionHandlers,
...indexerCategories.actionHandlers, ...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers, ...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers, ...notifications.actionHandlers,
...applications.actionHandlers, ...applications.actionHandlers,
...appProfiles.actionHandlers, ...appProfiles.actionHandlers,
@@ -89,7 +85,6 @@ export const reducers = createHandleActions({
...general.reducers, ...general.reducers,
...indexerCategories.reducers, ...indexerCategories.reducers,
...indexerProxies.reducers, ...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers, ...notifications.reducers,
...applications.reducers, ...applications.reducers,
...appProfiles.reducers, ...appProfiles.reducers,
@@ -15,27 +15,27 @@ const columns = [
}, },
{ {
name: 'commandName', name: 'commandName',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'queued', name: 'queued',
label: 'Queued', label: translate('Queued'),
isVisible: true isVisible: true
}, },
{ {
name: 'started', name: 'started',
label: 'Started', label: translate('Started'),
isVisible: true isVisible: true
}, },
{ {
name: 'ended', name: 'ended',
label: 'Ended', label: translate('Ended'),
isVisible: true isVisible: true
}, },
{ {
name: 'duration', name: 'duration',
label: 'Duration', label: translate('Duration'),
isVisible: true isVisible: true
}, },
{ {
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [ const columns = [
{ {
name: 'name', name: 'name',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'interval', name: 'interval',
label: 'Interval', label: translate('Interval'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastExecution', name: 'lastExecution',
label: 'Last Execution', label: translate('LastExecution'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastDuration', name: 'lastDuration',
label: 'Last Duration', label: translate('LastDuration'),
isVisible: true isVisible: true
}, },
{ {
name: 'nextExecution', name: 'nextExecution',
label: 'Next Execution', label: translate('NextExecution'),
isVisible: true isVisible: true
}, },
{ {
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -15,8 +16,11 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories; using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http namespace NzbDrone.Common.Test.Http
{ {
@@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost; private string _httpBinHost;
private string _httpBinHost2; private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp] [OneTimeSetUp]
public void FixtureSetUp() public void FixtureSetUp()
{ {
@@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com"; var mainHost = "httpbin.servarr.com";
// Use mirrors for tests that use two hosts // Use mirrors for tests that use two hosts
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" }; var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable. // httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHost = mainHost; _httpBinHost = mainHost;
@@ -46,31 +52,22 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available."); TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10; _httpBinSleep = 10;
} }
private bool IsTestSiteAvailable(string site) private bool IsTestSiteAvailable(string site)
{ {
try try
{ {
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest; var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK) if (res.StatusCode != HttpStatusCode.OK)
{ {
return false; return false;
} }
try res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429) if (res == null || res.StatusCode != HttpStatusCode.TooManyRequests)
{ {
return false; return false;
} }
@@ -95,10 +92,13 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0"); Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>()); Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>()); Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>()); Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>()); Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>()); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>()); Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[TestCase(CertificateValidationType.Enabled)]
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test] [Test]
public void should_execute_typed_get() public void should_execute_typed_get()
{ {
@@ -162,15 +184,45 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[TestCase("gzip")] [Test]
public void should_execute_get_using_gzip(string compression) public void should_execute_post_with_content_type()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/{compression}"); var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
response.Resource.Gzipped.Should().BeTrue(); response.Resource.Gzipped.Should().BeTrue();
response.Resource.Brotli.Should().BeFalse();
}
[Test]
[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)] [TestCase(HttpStatusCode.Unauthorized)]
@@ -190,6 +242,28 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test] [Test]
public void should_not_follow_redirects_when_not_in_production() public void should_not_follow_redirects_when_not_in_production()
{ {
@@ -315,13 +389,38 @@ namespace NzbDrone.Common.Test.Http
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file)); Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse(); File.Exists(file).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
[Test]
public void should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
using (var fileStream = new FileStream(file, FileMode.Create))
{
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
ExceptionVerification.ExpectedErrors(1);
File.Exists(file).Should().BeTrue();
var fileInfo = new FileInfo(file);
fileInfo.Length.Should().Be(0);
}
[Test] [Test]
public void should_send_cookie() public void should_send_cookie()
{ {
@@ -753,6 +852,7 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; } public string Url { get; set; }
public string Data { get; set; } public string Data { get; set; }
public bool Gzipped { get; set; } public bool Gzipped { get; set; }
public bool Brotli { get; set; }
} }
public class HttpCookieResource public class HttpCookieResource
@@ -98,15 +98,30 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Internal // Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")] [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("/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) public void should_clean_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret"); cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210"); 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(@"Some message (from 32.2.3.5 user agent)")]
[TestCase(@"Auth-Invalidated ip 32.2.3.5")] [TestCase(@"Auth-Invalidated ip 32.2.3.5")]
[TestCase(@"Auth-Success ip 32.2.3.5")] [TestCase(@"Auth-Success ip 32.2.3.5")]
@@ -210,5 +210,26 @@ namespace NzbDrone.Common.Extensions
return result.TrimStart(' ', '.').TrimEnd(' '); return result.TrimStart(' ', '.').TrimEnd(' ');
} }
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
} }
} }
@@ -0,0 +1,12 @@
using System.Net;
namespace NzbDrone.Common.Http
{
public class BasicNetworkCredential : NetworkCredential
{
public BasicNetworkCredential(string user, string pass)
: base(user, pass)
{
}
}
}
+2 -1
View File
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Http
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should // NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)"); private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' }; private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' }; private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -24,7 +25,7 @@ namespace NzbDrone.Common.Http
var matches = _CookieRegex.Match(cookieHeader); var matches = _CookieRegex.Match(cookieHeader);
while (matches.Success) while (matches.Success)
{ {
if (matches.Groups.Count > 2) if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
{ {
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value; cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
} }
@@ -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);
}
}
@@ -6,6 +6,5 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher public interface IHttpDispatcher
{ {
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies); Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
Task DownloadFileAsync(string url, string fileName);
} }
} }
@@ -1,13 +1,16 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NLog.Fluent; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
@@ -15,221 +18,225 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
public class ManagedHttpDispatcher : IHttpDispatcher public class ManagedHttpDispatcher : IHttpDispatcher
{ {
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy; private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder; private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo; private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly Logger _logger; private readonly ICached<CredentialCache> _credentialCache;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
{ {
_proxySettingsProvider = proxySettingsProvider; _proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy; _createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_userAgentBuilder = userAgentBuilder; _userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger; _httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
} }
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies) public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{ {
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
// Deflate is not a standard and could break depending on implementation. var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
// we should just stick with the more compatible Gzip if (cookieHeader.IsNotNullOrWhiteSpace())
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{ {
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); requestMessage.Headers.Add("Cookie", cookieHeader);
} }
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url); using var cts = new CancellationTokenSource();
if (request.RequestTimeout != TimeSpan.Zero)
{
cts.CancelAfter(request.RequestTimeout);
}
else
{
// The default for System.Net.Http.HttpClient
cts.CancelAfter(TimeSpan.FromSeconds(100));
}
if (request.Credentials != null)
{
if (request.Credentials is BasicNetworkCredential bc)
{
// Manually set header to avoid initial challenge response
var authInfo = bc.UserName + ":" + bc.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
}
else if (request.Credentials is NetworkCredential nc)
{
var creds = GetCredentialCache();
foreach (var authtype in new[] { "Basic", "Digest" })
{
creds.Remove((Uri)request.Url, authtype);
creds.Add((Uri)request.Url, authtype, nc);
}
}
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
if (request.Headers != null) if (request.Headers != null)
{ {
AddRequestHeaders(webRequest, request.Headers); AddRequestHeaders(requestMessage, request.Headers);
} }
HttpWebResponse httpWebResponse; var httpClient = GetClient(request.Url, request.ProxySettings);
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
try using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{ {
if (request.ContentData != null) byte[] data = null;
{
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
httpWebResponse = (HttpWebResponse)await webRequest.GetResponseAsync(); try
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{ {
// The default messages for WebException on mono are pretty horrible. if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{ {
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
}
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
} }
else else
{ {
throw; data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
} }
} }
} catch (Exception ex)
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
{ {
try throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
{ {
data = await responseStream.ToBytes(); try
} {
catch (Exception ex) cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
{ }
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); catch
{
// Ignore invalid cookies
}
} }
} }
}
sw.Stop(); var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, sw.ElapsedMilliseconds, httpWebResponse.StatusCode); sw.Stop();
}
public async Task DownloadFileAsync(string url, string fileName) return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
{
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
var uri = new HttpUri(url);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
await webClient.DownloadFileTaskAsync(url, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
}
catch (WebException e)
{
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
} }
} }
protected virtual IWebProxy GetProxy(HttpUri uri) protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri, 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) if (proxySettings != null)
{ {
proxy = _createManagedWebProxy.GetWebProxy(proxySettings); handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
} }
return proxy; var client = new System.Net.Http.HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
} }
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers) protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
{ {
foreach (var header in headers) foreach (var header in headers)
{ {
switch (header.Key) switch (header.Key)
{ {
case "Accept": case "Accept":
webRequest.Accept = header.Value; webRequest.Headers.Accept.ParseAdd(header.Value);
break; break;
case "Connection": case "Connection":
webRequest.Connection = header.Value; webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
break; break;
case "Content-Length": case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value); AddContentHeader(webRequest, "Content-Length", header.Value);
break; break;
case "Content-Type": case "Content-Type":
webRequest.ContentType = header.Value; AddContentHeader(webRequest, "Content-Type", header.Value);
break; break;
case "Date": case "Date":
webRequest.Date = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Expect": case "Expect":
webRequest.Expect = header.Value; webRequest.Headers.Expect.ParseAdd(header.Value);
break; break;
case "Host": case "Host":
webRequest.Host = header.Value; webRequest.Headers.Host = header.Value;
break; break;
case "If-Modified-Since": case "If-Modified-Since":
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Range": case "Range":
throw new NotImplementedException(); throw new NotImplementedException();
case "Referer": case "Referer":
webRequest.Referer = header.Value; webRequest.Headers.Add("Referer", header.Value);
break; break;
case "Transfer-Encoding": case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value; webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
break; break;
case "User-Agent": case "User-Agent":
webRequest.UserAgent = header.Value; webRequest.Headers.UserAgent.Clear();
webRequest.Headers.UserAgent.ParseAdd(header.Value);
break; break;
case "Proxy-Connection": case "Proxy-Connection":
throw new NotImplementedException(); throw new NotImplementedException();
@@ -239,5 +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;
}
}
} }
} }
-15
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;
}
}
}
+127 -29
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -86,13 +87,21 @@ namespace NzbDrone.Common.Http
} }
// 302 or 303 should default to GET on redirect even if POST on original // 302 or 303 should default to GET on redirect even if POST on original
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod) if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
{ {
request.Method = HttpMethod.Get; request.Method = HttpMethod.Get;
request.ContentData = null; request.ContentData = null;
} }
response = await ExecuteRequestAsync(request, cookieContainer); // Save to add to final response
var responseCookies = response.Cookies;
// Update cookiecontainer for next request with any cookies recieved on last request
var responseContainer = HandleRedirectCookies(request, response);
response = await ExecuteRequestAsync(request, responseContainer);
response.Cookies.Add(responseCookies);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
} }
@@ -102,11 +111,14 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
} }
if (!request.SuppressHttpError && response.HasHttpError) if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
{ {
_logger.Warn("HTTP Error - {0}", response); if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
if ((int)response.StatusCode == 429) if (response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(request, response); throw new TooManyRequestsException(request, response);
} }
@@ -124,6 +136,21 @@ namespace NzbDrone.Common.Http
return ExecuteAsync(request).GetAwaiter().GetResult(); return ExecuteAsync(request).GetAwaiter().GetResult();
} }
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
switch (statusCode)
{
case HttpStatusCode.Moved:
case HttpStatusCode.Found:
case HttpStatusCode.MultipleChoices:
return requestMethod == HttpMethod.Post;
case HttpStatusCode.SeeOther:
return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
default:
return false;
}
}
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer) private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)
@@ -140,8 +167,6 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer); var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer); HandleResponseCookies(response, cookieContainer);
@@ -208,52 +233,125 @@ namespace NzbDrone.Common.Http
} }
} }
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer) private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
{ {
// Don't collect persistnet cookies for intermediate/redirected urls. var sourceContainer = new CookieContainer();
/*lock (_cookieContainerCache) var responseCookies = response.GetCookies();
if (responseCookies.Count != 0)
{ {
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); foreach (var pair in responseCookies)
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); {
var existingCookies = cookieContainer.GetCookies((Uri)request.Url); Cookie cookie;
if (pair.Value == null)
{
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
cookieContainer.Add(persistentCookies); sourceContainer.Add((Uri)request.Url, cookie);
cookieContainer.Add(existingCookies); }
}*/ }
return sourceContainer;
} }
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer) private void HandleResponseCookies(HttpResponse response, CookieContainer container)
{ {
foreach (Cookie cookie in container.GetAllCookies())
{
cookie.Expired = true;
}
var cookieHeaders = response.GetCookieHeaders(); var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty()) if (cookieHeaders.Empty())
{ {
return; return;
} }
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie) if (response.Request.StoreResponseCookie)
{ {
lock (_cookieContainerCache) lock (_cookieContainerCache)
{ {
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
foreach (var cookieHeader in cookieHeaders) AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
{ }
try }
{ }
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
} private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
catch (Exception ex) {
{ foreach (var cookieHeader in cookieHeaders)
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url); {
} try
} {
container.SetCookies((Uri)url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", url);
} }
} }
} }
public async Task DownloadFileAsync(string url, string fileName) public async Task DownloadFileAsync(string url, string fileName)
{ {
await _httpDispatcher.DownloadFileAsync(url, fileName); var fileNamePart = fileName + ".part";
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new HttpException(request, response, "Site responded with html content.");
}
}
stopWatch.Stop();
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.Move(fileNamePart, fileName);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
finally
{
if (File.Exists(fileNamePart))
{
File.Delete(fileNamePart);
}
}
} }
public void DownloadFile(string url, string fileName) public void DownloadFile(string url, string fileName)
+45
View File
@@ -4,11 +4,27 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public static class WebHeaderCollectionExtensions
{
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
{
var result = new NameValueCollection();
foreach (var header in headers)
{
result.Add(header.Key, header.Value.ConcatToString(";"));
}
return result;
}
}
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
{ {
public HttpHeader(NameValueCollection headers) public HttpHeader(NameValueCollection headers)
@@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http
{ {
} }
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
public HttpHeader() public HttpHeader()
{ {
} }
@@ -107,6 +128,30 @@ namespace NzbDrone.Common.Http
} }
} }
public string ContentEncoding
{
get
{
return GetSingleValue("Content-Encoding");
}
set
{
SetSingleValue("Content-Encoding", value);
}
}
public string Vary
{
get
{
return GetSingleValue("Vary");
}
set
{
SetSingleValue("Vary", value);
}
}
public string UserAgent public string UserAgent
{ {
get get
+9 -8
View File
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
@@ -12,11 +14,13 @@ namespace NzbDrone.Common.Http
{ {
public HttpRequest(string url, HttpAccept httpAccept = null) public HttpRequest(string url, HttpAccept httpAccept = null)
{ {
Method = HttpMethod.Get;
Url = new HttpUri(url); Url = new HttpUri(url);
Headers = new HttpHeader(); Headers = new HttpHeader();
Method = HttpMethod.Get; Method = HttpMethod.Get;
ConnectionKeepAlive = true; ConnectionKeepAlive = true;
AllowAutoRedirect = true; AllowAutoRedirect = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction) if (!RuntimeInfo.IsProduction)
@@ -34,19 +38,23 @@ namespace NzbDrone.Common.Http
public HttpMethod Method { get; set; } public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; } public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; } public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; } public HttpProxySettings ProxySettings { get; set; }
public byte[] ContentData { get; set; } public byte[] ContentData { get; set; }
public string ContentSummary { get; set; } public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; } public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString() public override string ToString()
{ {
@@ -103,12 +111,5 @@ namespace NzbDrone.Common.Http
return encoding.GetString(ContentData); return encoding.GetString(ContentData);
} }
} }
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
Headers.Set("Authorization", "Basic " + authInfo);
}
} }
} }
+9 -12
View File
@@ -21,12 +21,13 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; } public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; } public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public NetworkCredential NetworkCredential { get; set; } public ICredentials NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
@@ -46,6 +47,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader(); Headers = new HttpHeader();
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>(); FormData = new List<HttpFormData>();
LogHttpError = true;
} }
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null) public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -106,6 +108,7 @@ namespace NzbDrone.Common.Http
request.Method = Method; request.Method = Method;
request.Encoding = Encoding; request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError; request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent; request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect; request.AllowAutoRedirect = AllowAutoRedirect;
request.StoreRequestCookie = StoreRequestCookie; request.StoreRequestCookie = StoreRequestCookie;
@@ -113,13 +116,7 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive; request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit; request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent; request.LogResponseContent = LogResponseContent;
request.Credentials = NetworkCredential;
if (NetworkCredential != null)
{
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers.Set("Authorization", "Basic " + authInfo);
}
foreach (var header in Headers) foreach (var header in Headers)
{ {
@@ -212,7 +209,7 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.UTF8.GetString(formData.ContentData)); summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.GetString(formData.ContentData));
} }
} }
@@ -232,9 +229,9 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.UTF8.GetString(v.ContentData)))); var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.GetString(v.ContentData))));
var urlencoded = string.Join("&", parameters); var urlencoded = string.Join("&", parameters);
var body = Encoding.UTF8.GetBytes(urlencoded); var body = Encoding.GetBytes(urlencoded);
request.Headers.ContentType = "application/x-www-form-urlencoded"; request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(body); request.SetContent(body);
@@ -406,7 +403,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData FormData.Add(new HttpFormData
{ {
Name = key, Name = key,
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)) ContentData = Encoding.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
}); });
return this; return this;
+7 -6
View File
@@ -64,10 +64,11 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400; public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved || public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found || StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.MultipleChoices ||
StatusCode == HttpStatusCode.PermanentRedirect ||
Headers.ContainsKey("Refresh"); Headers.ContainsKey("Refresh");
public string RedirectUrl public string RedirectUrl
@@ -88,13 +89,13 @@ namespace NzbDrone.Common.Http
if (match.Success) 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 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() public override string ToString()
{ {
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode); var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{ {
@@ -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);
}
}
}
@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), 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(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+2 -3
View File
@@ -4,14 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.18.0" /> <PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
@@ -0,0 +1,4 @@
{
"status": "success",
"response": []
}
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -11,6 +11,7 @@ using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http; using NzbDrone.Core.Http;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
@@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>())); Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger)); Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder()); Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
} }
@@ -60,6 +60,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.ImdbId.Should().Be(15569106); torrentInfo.ImdbId.Should().Be(15569106);
torrentInfo.TmdbId.Should().Be(135144); torrentInfo.TmdbId.Should().Be(135144);
torrentInfo.TvdbId.Should().Be(410548); torrentInfo.TvdbId.Should().Be(410548);
torrentInfo.Languages.Should().HaveCount(1);
torrentInfo.Languages.First().Should().Be("Japanese");
torrentInfo.Subs.Should().HaveCount(27);
torrentInfo.Subs.First().Should().Be("Arabic");
} }
} }
} }
@@ -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);
}
}
}
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
}; };
Mocker.GetMock<IRarbgTokenProvider>() Mocker.GetMock<IRarbgTokenProvider>()
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), It.IsAny<string>())) .Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
.Returns("validtoken"); .Returns("validtoken");
} }
@@ -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);
}
}
}
@@ -3,7 +3,6 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -16,7 +15,7 @@ namespace NzbDrone.Core.Test.Localization
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English); Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns("en");
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory); Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
} }
@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
@@ -6,7 +6,6 @@ using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Security; using NzbDrone.Core.Security;
@@ -139,9 +138,9 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableColorImpairedMode", value); } set { SetValue("EnableColorImpairedMode", value); }
} }
public int UILanguage public string UILanguage
{ {
get { return GetValueInt("UILanguage", (int)Language.English); } get { return GetValue("UILanguage", "en"); }
set { SetValue("UILanguage", value); } set { SetValue("UILanguage", value); }
} }
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Configuration
string TimeFormat { get; set; } string TimeFormat { get; set; }
bool ShowRelativeDates { get; set; } bool ShowRelativeDates { get; set; }
bool EnableColorImpairedMode { get; set; } bool EnableColorImpairedMode { get; set; }
int UILanguage { get; set; } string UILanguage { get; set; }
//Internal //Internal
string PlexClientIdentifier { get; } string PlexClientIdentifier { get; }
@@ -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);
}
}
}
+32
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Data.Common; using System.Data.Common;
using System.Data.SQLite; using System.Data.SQLite;
using System.Net.Sockets;
using NLog; using NLog;
using Npgsql; using Npgsql;
using NzbDrone.Common.Disk; 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); 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) catch (Exception e)
{ {
throw new ProwlarrStartupException(e, "Error creating main database"); throw new ProwlarrStartupException(e, "Error creating main database");
@@ -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" },
};
}
}
@@ -14,7 +14,6 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerVersions; using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs; using NzbDrone.Core.Jobs;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -113,8 +112,6 @@ namespace NzbDrone.Core.Datastore
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());
SqlMapper.AddTypeHandler(new DapperLanguageIntConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<Language>>(new LanguageIntConverter()));
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<HashSet<int>>());
@@ -1,111 +1,161 @@
using CookComputing.XmlRpc; using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2 namespace NzbDrone.Core.Download.Clients.Aria2
{ {
public class Aria2Version public class Aria2Fault
{ {
[XmlRpcMember("version")] public Aria2Fault(XElement element)
public string Version; {
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").ElementAsInt("int");
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("enabledFeatures")] public int FaultCode { get; set; }
public string[] EnabledFeatures; public string FaultString { get; set; }
} }
public class Aria2Uri public class Aria2Version
{ {
[XmlRpcMember("status")] public Aria2Version(XElement element)
public string Status; {
foreach (var e in element.XPathSelectElements("./struct/member"))
{
if (e.ElementAsString("name") == "version")
{
Version = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("uri")] public string Version { get; set; }
public string Uri;
} }
public class Aria2File public class Aria2File
{ {
[XmlRpcMember("index")] public Aria2File(XElement element)
public string Index; {
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("length")] if (name == "path")
public string Length; {
Path = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("completedLength")] public string Path { get; set; }
public string CompletedLength; }
[XmlRpcMember("path")] public class Aria2Dict
public string Path; {
public Aria2Dict(XElement element)
{
Dict = new Dictionary<string, string>();
[XmlRpcMember("selected")] foreach (var e in element.XPathSelectElements("./struct/member"))
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public string Selected; Dict.Add(e.ElementAsString("name"), e.Element("value").GetStringValue());
}
}
[XmlRpcMember("uris")] public Dictionary<string, string> Dict { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] }
public Aria2Uri[] Uris;
public class Aria2Bittorrent
{
public Aria2Bittorrent(XElement element)
{
foreach (var e in element.Descendants("member"))
{
if (e.ElementAsString("name") == "name")
{
Name = e.Element("value").GetStringValue();
}
}
}
public string Name;
} }
public class Aria2Status public class Aria2Status
{ {
[XmlRpcMember("bittorrent")] public Aria2Status(XElement element)
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public XmlRpcStruct Bittorrent; foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("bitfield")] if (name == "bittorrent")
[XmlRpcMissingMapping(MappingAction.Ignore)] {
public string Bitfield; Bittorrent = new Aria2Bittorrent(e.Element("value"));
}
else if (name == "infoHash")
{
InfoHash = e.Element("value").GetStringValue();
}
else if (name == "completedLength")
{
CompletedLength = e.Element("value").GetStringValue();
}
else if (name == "downloadSpeed")
{
DownloadSpeed = e.Element("value").GetStringValue();
}
else if (name == "files")
{
Files = e.XPathSelectElement("./value/array/data")
.Elements()
.Select(x => new Aria2File(x))
.ToArray();
}
else if (name == "gid")
{
Gid = e.Element("value").GetStringValue();
}
else if (name == "status")
{
Status = e.Element("value").GetStringValue();
}
else if (name == "totalLength")
{
TotalLength = e.Element("value").GetStringValue();
}
else if (name == "uploadLength")
{
UploadLength = e.Element("value").GetStringValue();
}
else if (name == "errorMessage")
{
ErrorMessage = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("infoHash")] public Aria2Bittorrent Bittorrent { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string InfoHash { get; set; }
public string InfoHash; public string CompletedLength { get; set; }
public string DownloadSpeed { get; set; }
[XmlRpcMember("completedLength")] public Aria2File[] Files { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string Gid { get; set; }
public string CompletedLength; public string Status { get; set; }
public string TotalLength { get; set; }
[XmlRpcMember("connections")] public string UploadLength { get; set; }
[XmlRpcMissingMapping(MappingAction.Ignore)] public string ErrorMessage { get; set; }
public string Connections;
[XmlRpcMember("dir")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Dir;
[XmlRpcMember("downloadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string DownloadSpeed;
[XmlRpcMember("files")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2File[] Files;
[XmlRpcMember("gid")]
public string Gid;
[XmlRpcMember("numPieces")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string NumPieces;
[XmlRpcMember("pieceLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string PieceLength;
[XmlRpcMember("status")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Status;
[XmlRpcMember("totalLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string TotalLength;
[XmlRpcMember("uploadLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadLength;
[XmlRpcMember("uploadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadSpeed;
[XmlRpcMember("errorMessage")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string ErrorMessage;
} }
} }
@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Linq;
using CookComputing.XmlRpc; using System.Xml.Linq;
using NLog; using System.Xml.XPath;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2 namespace NzbDrone.Core.Download.Clients.Aria2
{ {
@@ -12,46 +12,103 @@ namespace NzbDrone.Core.Download.Clients.Aria2
string GetVersion(Aria2Settings settings); string GetVersion(Aria2Settings settings);
string AddUri(Aria2Settings settings, string magnet); string AddUri(Aria2Settings settings, string magnet);
string AddTorrent(Aria2Settings settings, byte[] torrent); string AddTorrent(Aria2Settings settings, byte[] torrent);
Dictionary<string, string> GetGlobals(Aria2Settings settings);
List<Aria2Status> GetTorrents(Aria2Settings settings);
Aria2Status GetFromGID(Aria2Settings settings, string gid); Aria2Status GetFromGID(Aria2Settings settings, string gid);
} }
public interface IAria2 : IXmlRpcProxy
{
[XmlRpcMethod("aria2.getVersion")]
Aria2Version GetVersion(string token);
[XmlRpcMethod("aria2.addUri")]
string AddUri(string token, string[] uri);
[XmlRpcMethod("aria2.addTorrent")]
string AddTorrent(string token, byte[] torrent);
[XmlRpcMethod("aria2.forceRemove")]
string Remove(string token, string gid);
[XmlRpcMethod("aria2.tellStatus")]
Aria2Status GetFromGid(string token, string gid);
[XmlRpcMethod("aria2.getGlobalOption")]
XmlRpcStruct GetGlobalOption(string token);
[XmlRpcMethod("aria2.tellActive")]
Aria2Status[] GetActives(string token);
[XmlRpcMethod("aria2.tellWaiting")]
Aria2Status[] GetWaitings(string token, int offset, int num);
[XmlRpcMethod("aria2.tellStopped")]
Aria2Status[] GetStoppeds(string token, int offset, int num);
}
public class Aria2Proxy : IAria2Proxy public class Aria2Proxy : IAria2Proxy
{ {
private readonly Logger _logger; private readonly IHttpClient _httpClient;
public Aria2Proxy(Logger logger) public Aria2Proxy(IHttpClient httpClient)
{ {
_logger = logger; _httpClient = httpClient;
}
public string GetVersion(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getVersion", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var version = new Aria2Version(element);
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
var response = ExecuteRequest(settings, "aria2.tellStatus", GetToken(settings), gid);
var element = response.XPathSelectElement("./methodResponse/params/param/value");
return new Aria2Status(element);
}
private List<Aria2Status> GetTorrentsMethod(Aria2Settings settings, string method, params object[] args)
{
var allArgs = new List<object> { GetToken(settings) };
if (args.Any())
{
allArgs.AddRange(args);
}
var response = ExecuteRequest(settings, method, allArgs.ToArray());
var element = response.XPathSelectElement("./methodResponse/params/param/value/array/data");
var torrents = element?.Elements()
.Select(x => new Aria2Status(x))
.ToList()
?? new List<Aria2Status>();
return torrents;
}
public List<Aria2Status> GetTorrents(Aria2Settings settings)
{
var active = GetTorrentsMethod(settings, "aria2.tellActive");
var waiting = GetTorrentsMethod(settings, "aria2.tellWaiting", 0, 10 * 1024);
var stopped = GetTorrentsMethod(settings, "aria2.tellStopped", 0, 10 * 1024);
var items = new List<Aria2Status>();
items.AddRange(active);
items.AddRange(waiting);
items.AddRange(stopped);
return items;
}
public Dictionary<string, string> GetGlobals(Aria2Settings settings)
{
var response = ExecuteRequest(settings, "aria2.getGlobalOption", GetToken(settings));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
var result = new Aria2Dict(element);
return result.Dict;
}
public string AddUri(Aria2Settings settings, string magnet)
{
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var gid = response.GetStringResponse();
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
var gid = response.GetStringResponse();
return gid;
} }
private string GetToken(Aria2Settings settings) private string GetToken(Aria2Settings settings)
@@ -59,86 +116,29 @@ namespace NzbDrone.Core.Download.Clients.Aria2
return $"token:{settings?.SecretToken}"; return $"token:{settings?.SecretToken}";
} }
private string GetURL(Aria2Settings settings) private XDocument ExecuteRequest(Aria2Settings settings, string methodName, params object[] args)
{ {
return $"http{(settings.UseSsl ? "s" : "")}://{settings.Host}:{settings.Port}{settings.RpcPath}"; var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
}
public string GetVersion(Aria2Settings settings)
{
_logger.Debug("> aria2.getVersion");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion(GetToken(settings)));
_logger.Debug("< aria2.getVersion");
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
_logger.Debug("> aria2.tellStatus");
var client = BuildClient(settings);
var found = ExecuteRequest(() => client.GetFromGid(GetToken(settings), gid));
_logger.Debug("< aria2.tellStatus");
return found;
}
public string AddUri(Aria2Settings settings, string magnet)
{
_logger.Debug("> aria2.addUri");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddUri(GetToken(settings), new[] { magnet }));
_logger.Debug("< aria2.addUri");
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
_logger.Debug("> aria2.addTorrent");
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddTorrent(GetToken(settings), torrent));
_logger.Debug("< aria2.addTorrent");
return gid;
}
private IAria2 BuildClient(Aria2Settings settings)
{
var client = XmlRpcProxyGen.Create<IAria2>();
client.Url = GetURL(settings);
return client;
}
private T ExecuteRequest<T>(Func<T> task)
{
try
{ {
return task(); LogResponseContent = true,
} };
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to aria2, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to aria2, certificate validation failed.", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to aria2, please check your settings", ex); var request = requestBuilder.Call(methodName, args).Build();
var response = _httpClient.Execute(request);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new Aria2Fault(faultElement);
throw new DownloadClientException($"Aria2 returned error code {fault.FaultCode}: {fault.FaultString}");
} }
return doc;
} }
} }
} }
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
baseUrl = HttpUri.CombinePath(baseUrl, "api"); baseUrl = HttpUri.CombinePath(baseUrl, "api");
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate"); requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate");
var httpRequest = requestBuilder.Build(); var httpRequest = requestBuilder.Build();
@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters); var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
var httpRequest = requestBuilder.Build(); var httpRequest = requestBuilder.Build();
@@ -276,7 +276,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{ {
LogResponseContent = true, LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password) NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
}; };
return requestBuilder; return requestBuilder;
} }
@@ -317,7 +317,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{ {
LogResponseContent = true, LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password) NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
}; };
return requestBuilder; return requestBuilder;
} }
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.AllowAutoRedirect = false; requestBuilder.AllowAutoRedirect = false;
return requestBuilder; return requestBuilder;
@@ -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; }
}
}
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using CookComputing.XmlRpc; using System.Xml.Linq;
using NLog; using System.Xml.XPath;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
@@ -18,122 +20,70 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void RemoveTorrent(string hash, RTorrentSettings settings); void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings); void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, RTorrentSettings settings); bool HasHashTorrent(string hash, RTorrentSettings settings);
} void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
public interface IRTorrent : IXmlRpcProxy
{
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
[XmlRpcMethod("d.erase")]
int Remove(string hash);
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("system.client_version")]
string GetVersion();
} }
public class RTorrentProxy : IRTorrentProxy public class RTorrentProxy : IRTorrentProxy
{ {
private readonly Logger _logger; private readonly IHttpClient _httpClient;
public RTorrentProxy(Logger logger) public RTorrentProxy(IHttpClient httpClient)
{ {
_logger = logger; _httpClient = httpClient;
} }
public string GetVersion(RTorrentSettings settings) public string GetVersion(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: system.client_version"); var document = ExecuteRequest(settings, "system.client_version");
var client = BuildClient(settings); return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
var version = ExecuteRequest(() => client.GetVersion());
return version;
} }
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings) public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.multicall2"); var document = ExecuteRequest(settings,
"d.multicall2",
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=", //long
"d.timestamp.finished="); // long (unix timestamp)
var client = BuildClient(settings); var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
var ret = ExecuteRequest(() => client.TorrentMulticall( ?.Elements()
"", .Select(x => new RTorrentTorrent(x))
"", .ToList()
"d.name=", // string ?? new List<RTorrentTorrent>();
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=")); //long
_logger.Trace(ret.ToJson()); return torrents;
var items = new List<RTorrentTorrent>();
foreach (object[] torrent in ret)
{
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
var item = new RTorrentTorrent();
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
items.Add(item);
}
return items;
} }
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", torrentUrl };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.normal");
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.start");
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
}
});
if (response != 0) XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.normal", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl); throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
} }
@@ -141,22 +91,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings) public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{ {
var client = BuildClient(settings); var args = new List<object> { "", fileContent };
var response = ExecuteRequest(() => args.AddRange(GetCommands(label, priority, directory));
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.raw");
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.raw_start");
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
}
});
if (response != 0) XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.raw", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", fileName); throw new DownloadClientException("Could not add torrent: {0}.", fileName);
} }
@@ -164,25 +113,29 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings) public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.custom1.set"); var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
var client = BuildClient(settings); if (response.GetStringResponse() != label)
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
{ {
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label); throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
} }
} }
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{
var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
}
}
public void RemoveTorrent(string hash, RTorrentSettings settings) public void RemoveTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.erase"); var response = ExecuteRequest(settings, "d.erase", hash);
var client = BuildClient(settings); if (response.GetIntResponse() != 0)
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0)
{ {
throw new DownloadClientException("Could not remove torrent: {0}.", hash); throw new DownloadClientException("Could not remove torrent: {0}.", hash);
} }
@@ -190,13 +143,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public bool HasHashTorrent(string hash, RTorrentSettings settings) public bool HasHashTorrent(string hash, RTorrentSettings settings)
{ {
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try try
{ {
var name = ExecuteRequest(() => client.GetName(hash)); var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
if (name.IsNullOrWhiteSpace()) if (name.IsNullOrWhiteSpace())
{ {
@@ -235,45 +185,34 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray(); return result.ToArray();
} }
private IRTorrent BuildClient(RTorrentSettings settings) private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
{ {
var client = XmlRpcProxyGen.Create<IRTorrent>(); var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
client.Url = string.Format(@"{0}://{1}:{2}/{3}", LogResponseContent = true,
settings.UseSsl ? "https" : "http", };
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
if (!settings.Username.IsNullOrWhiteSpace()) if (!settings.Username.IsNullOrWhiteSpace())
{ {
client.Credentials = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
} }
return client; var request = requestBuilder.Call(methodName, args).Build();
}
private T ExecuteRequest<T>(Func<T> task) var response = _httpClient.Execute(request);
{
try
{
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex); var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new RTorrentFault(faultElement);
throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
} }
return doc;
} }
} }
} }
@@ -1,7 +1,35 @@
namespace NzbDrone.Core.Download.Clients.RTorrent using System;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{ {
public class RTorrentTorrent public class RTorrentTorrent
{ {
public RTorrentTorrent()
{
}
public RTorrentTorrent(XElement element)
{
var data = element.Descendants("value").ToList();
Name = data[0].GetStringValue();
Hash = data[1].GetStringValue();
Path = data[2].GetStringValue();
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
TotalSize = data[4].GetLongValue();
RemainingSize = data[5].GetLongValue();
DownRate = data[6].GetLongValue();
Ratio = data[7].GetLongValue();
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
IsActive = Convert.ToBoolean(data[9].GetLongValue());
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
FinishedTime = data[11].GetLongValue();
}
public string Name { get; set; } public string Name { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string Path { get; set; } public string Path { get; set; }
@@ -10,6 +38,7 @@
public long RemainingSize { get; set; } public long RemainingSize { get; set; }
public long DownRate { get; set; } public long DownRate { get; set; }
public long Ratio { get; set; } public long Ratio { get; set; }
public long FinishedTime { get; set; }
public bool IsFinished { get; set; } public bool IsFinished { get; set; }
public bool IsOpen { get; set; } public bool IsOpen { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true; requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password); requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
return requestBuilder; return requestBuilder;
} }
@@ -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();
}
}
}
@@ -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");
}
}
}
@@ -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());
}
}
}
@@ -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");
}
}
@@ -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; }
}
}
@@ -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;
}
}
}
@@ -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")
};
}
}
}
@@ -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;
}
}
}
@@ -10,7 +10,9 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@@ -18,12 +20,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
{ {
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings> public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{ {
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx" };
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
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) : base(cloudRequestBuilder, httpClient, logger, localizationService)
{ {
_proxySettingsProvider = proxySettingsProvider;
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent"); _cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
} }
@@ -45,7 +48,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public override HttpResponse PostResponse(HttpResponse response) public override HttpResponse PostResponse(HttpResponse response)
{ {
if (!IsCloudflareProtected(response)) if (!CloudFlareDetectionService.IsCloudflareProtected(response))
{ {
_logger.Debug("CF Protection not detected, returning original response"); _logger.Debug("CF Protection not detected, returning original response");
return response; return response;
@@ -53,14 +56,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
var flaresolverrResponse = _httpClient.Execute(GenerateFlareSolverrRequest(response.Request)); var flaresolverrResponse = _httpClient.Execute(GenerateFlareSolverrRequest(response.Request));
FlareSolverrResponse result = null;
if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError) if (flaresolverrResponse.StatusCode != HttpStatusCode.OK && flaresolverrResponse.StatusCode != HttpStatusCode.InternalServerError)
{ {
throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + response.StatusCode); throw new FlareSolverrException("HTTP StatusCode not 200 or 500. Status is :" + response.StatusCode);
} }
result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content); var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content);
var newRequest = response.Request; var newRequest = response.Request;
@@ -70,26 +71,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
InjectCookies(newRequest, result); InjectCookies(newRequest, result);
//Request again with User-Agent and Cookies from Flaresolvrr //Request again with User-Agent and Cookies from Flaresolverr
var finalResponse = _httpClient.Execute(newRequest); var finalResponse = _httpClient.Execute(newRequest);
return finalResponse; return finalResponse;
} }
private static bool IsCloudflareProtected(HttpResponse response)
{
// check status code
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden))
{
// check response headers
return response.Headers.Any(i =>
i.Key != null && i.Key.ToLower() == "server" && CloudflareServerNames.Contains(i.Value.ToLower()));
}
return false;
}
private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse) private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse)
{ {
var rCookies = flareSolverrResponse.Solution.Cookies; var rCookies = flareSolverrResponse.Solution.Cookies;
@@ -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 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; 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) if (request.Method == HttpMethod.Get)
{ {
req = new FlareSolverrRequestGet req = new FlareSolverrRequestGet
@@ -123,14 +114,18 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
Cmd = "request.get", Cmd = "request.get",
Url = url, Url = url,
MaxTimeout = maxTimeout, MaxTimeout = maxTimeout,
UserAgent = userAgent UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
}; };
} }
else if (request.Method == HttpMethod.Post) else if (request.Method == HttpMethod.Post)
{ {
var contentTypeType = request.Headers.ContentType; var contentTypeType = request.Headers.ContentType.ToLower() ?? "<null>";
if (contentTypeType == "application/x-www-form-urlencoded") if (contentTypeType.Contains("application/x-www-form-urlencoded"))
{ {
var contentTypeValue = request.Headers.ContentType.ToString(); var contentTypeValue = request.Headers.ContentType.ToString();
var postData = request.GetContent(); var postData = request.GetContent();
@@ -146,10 +141,15 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
ContentLength = null ContentLength = null
}, },
MaxTimeout = maxTimeout, 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 //TODO Implement - check if we just need to pass the content-type with the relevant headers
throw new FlareSolverrException("Unimplemented POST Content-Type: " + request.Headers.ContentType); throw new FlareSolverrException("Unimplemented POST Content-Type: " + request.Headers.ContentType);
@@ -169,9 +169,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
newRequest.Headers.ContentType = "application/json"; newRequest.Headers.ContentType = "application/json";
newRequest.Method = HttpMethod.Post; newRequest.Method = HttpMethod.Post;
newRequest.LogResponseContent = true;
newRequest.SetContent(req.ToJson()); newRequest.SetContent(req.ToJson());
_logger.Debug("Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Cloudflare Detected, Applying FlareSolverr Proxy {0} to request {1}", Name, request.Url);
return newRequest; return newRequest;
} }
@@ -205,38 +206,59 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
return new ValidationResult(failures); 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 Cmd { get; set; }
public string Url { get; set; } public string Url { get; set; }
public string UserAgent { get; set; } public string UserAgent { get; set; }
public Cookie[] Cookies { 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 string Headers { get; set; }
public int MaxTimeout { get; set; } public int MaxTimeout { get; set; }
} }
public class FlareSolverrRequestPost : FlareSolverrRequest private class FlareSolverrRequestPost : FlareSolverrRequest
{ {
public string PostData { get; set; } public string PostData { get; set; }
public int MaxTimeout { get; set; } public int MaxTimeout { get; set; }
} }
public class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost private class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
{ {
public HeadersPost Headers { get; set; } 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 ContentType { get; set; }
public string ContentLength { get; set; } public string ContentLength { get; set; }
} }
public class FlareSolverrResponse private class FlareSolverrResponse
{ {
public string Status { get; set; } public string Status { get; set; }
public string Message { get; set; } public string Message { get; set; }
@@ -246,7 +268,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public Solution Solution { get; set; } public Solution Solution { get; set; }
} }
public class Solution private class Solution
{ {
public string Url { get; set; } public string Url { get; set; }
public string Status { get; set; } public string Status { get; set; }
@@ -256,7 +278,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public string UserAgent { get; set; } public string UserAgent { get; set; }
} }
public class Cookie private class Cookie
{ {
public string Name { get; set; } public string Name { get; set; }
public string Value { 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 System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value);
} }
public class Headers private class Headers
{ {
public string Status { get; set; } public string Status { get; set; }
public string Date { get; set; } public string Date { get; set; }
@@ -3,7 +3,9 @@ using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.Notifications.Prowl;
namespace NzbDrone.Core.IndexerProxies.Http namespace NzbDrone.Core.IndexerProxies.Http
{ {
@@ -18,14 +20,13 @@ namespace NzbDrone.Core.IndexerProxies.Http
public override HttpRequest PreRequest(HttpRequest request) public override HttpRequest PreRequest(HttpRequest request)
{ {
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace()) request.ProxySettings = new HttpProxySettings(ProxyType.Http,
{ Settings.Host,
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null, new NetworkCredential(Settings.Username, Settings.Password)); Settings.Port,
} null,
else false,
{ Settings.Username,
request.Proxy = new WebProxy(Settings.Host + ":" + Settings.Port, false, null); Settings.Password);
}
_logger.Debug("Applying HTTP(S) Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Applying HTTP(S) Proxy {0} to request {1}", Name, request.Url);
@@ -4,6 +4,7 @@ using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
namespace NzbDrone.Core.IndexerProxies.Socks4 namespace NzbDrone.Core.IndexerProxies.Socks4
@@ -25,14 +26,13 @@ namespace NzbDrone.Core.IndexerProxies.Socks4
return null; return null;
} }
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace()) request.ProxySettings = new HttpProxySettings(ProxyType.Socks4,
{ Settings.Host,
request.Proxy = new WebProxy(uri, false, null, new NetworkCredential(Settings.Username, Settings.Password)); Settings.Port,
} null,
else false,
{ Settings.Username,
request.Proxy = new WebProxy(uri); Settings.Password);
}
_logger.Debug("Applying Socks4 Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Applying Socks4 Proxy {0} to request {1}", Name, request.Url);
@@ -4,6 +4,7 @@ using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
namespace NzbDrone.Core.IndexerProxies.Socks5 namespace NzbDrone.Core.IndexerProxies.Socks5
@@ -26,14 +27,13 @@ namespace NzbDrone.Core.IndexerProxies.Socks5
return null; return null;
} }
if (Settings.Username.IsNotNullOrWhiteSpace() && Settings.Password.IsNotNullOrWhiteSpace()) request.ProxySettings = new HttpProxySettings(ProxyType.Socks5,
{ Settings.Host,
request.Proxy = new WebProxy(uri, false, null, new NetworkCredential(Settings.Username, Settings.Password)); Settings.Port,
} null,
else false,
{ Settings.Username,
request.Proxy = new WebProxy(uri); Settings.Password);
}
_logger.Debug("Applying Socks5 Proxy {0} to request {1}", Name, request.Url); _logger.Debug("Applying Socks5 Proxy {0} to request {1}", Name, request.Url);
@@ -6,6 +6,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{ {
public string Author { get; set; } public string Author { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Publisher { get; set; }
public int? Year { get; set; }
public string Genre { get; set; }
public override bool RssSearch public override bool RssSearch
{ {
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int? TmdbId { get; set; } public int? TmdbId { get; set; }
public int? TraktId { get; set; } public int? TraktId { get; set; }
public int? DoubanId { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public string Genre { get; set; } public string Genre { get; set; }
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Artist { get; set; } public string Artist { get; set; }
public string Label { get; set; } public string Label { get; set; }
public string Genre { get; set; } public string Genre { get; set; }
public string Track { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public override bool RssSearch public override bool RssSearch
@@ -17,6 +17,9 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? TvMazeId { get; set; } public int? TvMazeId { get; set; }
public int? TraktId { get; set; } public int? TraktId { get; set; }
public int? TmdbId { get; set; } public int? TmdbId { get; set; }
public int? DoubanId { get; set; }
public int? Year { get; set; }
public string Genre { get; set; }
public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim(); public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim();
public string EpisodeSearchString => GetEpisodeSearchString(); public string EpisodeSearchString => GetEpisodeSearchString();
@@ -4,10 +4,10 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public class NewznabRequest public class NewznabRequest
{ {
private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex TvRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tvdbid\:)(?<tvdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:season\:)(?<season>[^{]+)|(?:episode\:)(?<episode>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MovieRegex = new Regex(@"\{((?:imdbid\:)(?<imdbid>[^{]+)|(?:doubanid\:)(?<doubanid>[^{]+)|(?:tmdbid\:)(?<tmdbid>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MusicRegex = new Regex(@"\{((?:artist\:)(?<artist>[^{]+)|(?:album\:)(?<album>[^{]+)|(?:track\:)(?<track>[^{]+)|(?:label\:)(?<label>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex BookRegex = new Regex(@"\{((?:author\:)(?<author>[^{]+)|(?:publisher\:)(?<publisher>[^{]+)|(?:title\:)(?<title>[^{]+))\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public string t { get; set; } public string t { get; set; }
public string q { get; set; } public string q { get; set; }
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.IndexerSearch
public int? tvmazeid { get; set; } public int? tvmazeid { get; set; }
public int? traktid { get; set; } public int? traktid { get; set; }
public int? tvdbid { get; set; } public int? tvdbid { get; set; }
public int? doubanid { get; set; }
public int? season { get; set; } public int? season { get; set; }
public string ep { get; set; } public string ep { get; set; }
public string album { get; set; } public string album { get; set; }
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.IndexerSearch
public string genre { get; set; } public string genre { get; set; }
public string author { get; set; } public string author { get; set; }
public string title { get; set; } public string title { get; set; }
public string publisher { get; set; }
public string configured { get; set; } public string configured { get; set; }
public string source { get; set; } public string source { get; set; }
public string host { get; set; } public string host { get; set; }
@@ -49,6 +51,16 @@ namespace NzbDrone.Core.IndexerSearch
tvdbid = int.TryParse(match.Groups["tvdbid"].Value, out var tvdb) ? tvdb : null; tvdbid = int.TryParse(match.Groups["tvdbid"].Value, out var tvdb) ? tvdb : null;
} }
if (match.Groups["tmdbid"].Success)
{
tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["season"].Success) if (match.Groups["season"].Success)
{ {
season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null; season = int.TryParse(match.Groups["season"].Value, out var seasonParsed) ? seasonParsed : null;
@@ -79,6 +91,11 @@ namespace NzbDrone.Core.IndexerSearch
tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null; tmdbid = int.TryParse(match.Groups["tmdbid"].Value, out var tmdb) ? tmdb : null;
} }
if (match.Groups["doubanid"].Success)
{
doubanid = int.TryParse(match.Groups["doubanid"].Value, out var tmdb) ? tmdb : null;
}
if (match.Groups["imdbid"].Success) if (match.Groups["imdbid"].Success)
{ {
imdbid = match.Groups["imdbid"].Value; imdbid = match.Groups["imdbid"].Value;
@@ -104,6 +121,11 @@ namespace NzbDrone.Core.IndexerSearch
album = match.Groups["album"].Value; album = match.Groups["album"].Value;
} }
if (match.Groups["track"].Success)
{
track = match.Groups["track"].Value;
}
if (match.Groups["label"].Success) if (match.Groups["label"].Success)
{ {
label = match.Groups["label"].Value; label = match.Groups["label"].Value;
@@ -129,6 +151,11 @@ namespace NzbDrone.Core.IndexerSearch
title = match.Groups["title"].Value; title = match.Groups["title"].Value;
} }
if (match.Groups["publisher"].Success)
{
publisher = match.Groups["publisher"].Value;
}
q = q.Replace(match.Value, "").Trim(); q = q.Replace(match.Value, "").Trim();
} }
} }
@@ -90,8 +90,8 @@ namespace NzbDrone.Core.IndexerSearch
new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")), new XAttribute("type", protocol == DownloadProtocol.Torrent ? "application/x-bittorrent" : "application/x-nzb")),
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol), r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol), r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c.Id, protocol), r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol),
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c.Id, protocol), r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol),
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol), r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
GetNabElement("rageid", r.TvRageId, protocol), GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol), GetNabElement("tvdbid", r.TvdbId, protocol),
@@ -107,6 +107,8 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol), GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol), GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol),
GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol), GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol),
GetNabElement("label", RemoveInvalidXMLChars(r.Label), protocol),
GetNabElement("track", RemoveInvalidXMLChars(r.Track), protocol),
GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol), GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol),
GetNabElement("minimumratio", t.MinimumRatio, protocol), GetNabElement("minimumratio", t.MinimumRatio, protocol),
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol), GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),
@@ -1,13 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using NLog; using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Analytics; using NzbDrone.Core.Analytics;
using NzbDrone.Core.Indexers.Events; using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
@@ -16,26 +20,48 @@ namespace NzbDrone.Core.IndexerSearch
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _requestBuilder; private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IAnalyticsService _analyticsService; private readonly IAnalyticsService _analyticsService;
private readonly Debouncer _debouncer;
private readonly Logger _logger; private readonly Logger _logger;
private readonly List<ReleaseInfo> _pendingUpdates;
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger) public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
{ {
_debouncer = new Debouncer(SendReleases, TimeSpan.FromMinutes(10));
_analyticsService = analyticsService; _analyticsService = analyticsService;
_requestBuilder = requestBuilder.Releases; _requestBuilder = requestBuilder.Releases;
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger; _logger = logger;
_pendingUpdates = new List<ReleaseInfo>();
} }
public void HandleAsync(IndexerQueryEvent message) public void HandleAsync(IndexerQueryEvent message)
{ {
if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null) if (_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(); var request = _requestBuilder.Create().Resource("release/push").Build();
request.Method = HttpMethod.Post; request.Method = HttpMethod.Post;
request.Headers.ContentType = "application/json"; request.Headers.ContentType = "application/json";
request.SuppressHttpError = true; request.SuppressHttpError = true;
request.LogHttpError = false;
var body = message.QueryResult.Releases.Select(x => new var body = pendingUpdates.DistinctBy(r => r.Title).Select(x => new
{ {
Title = x.Title, Title = x.Title,
Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(), Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(),
@@ -59,6 +59,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.ImdbId = request.imdbid; searchSpec.ImdbId = request.imdbid;
searchSpec.TmdbId = request.tmdbid; searchSpec.TmdbId = request.tmdbid;
searchSpec.TraktId = request.traktid; searchSpec.TraktId = request.traktid;
searchSpec.DoubanId = request.doubanid;
searchSpec.Year = request.year; searchSpec.Year = request.year;
searchSpec.Genre = request.genre; searchSpec.Genre = request.genre;
@@ -73,6 +74,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Album = request.album; searchSpec.Album = request.album;
searchSpec.Label = request.label; searchSpec.Label = request.label;
searchSpec.Genre = request.genre; searchSpec.Genre = request.genre;
searchSpec.Track = request.track;
searchSpec.Year = request.year; searchSpec.Year = request.year;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
@@ -88,8 +90,11 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.ImdbId = request.imdbid; searchSpec.ImdbId = request.imdbid;
searchSpec.TraktId = request.traktid; searchSpec.TraktId = request.traktid;
searchSpec.TmdbId = request.tmdbid; searchSpec.TmdbId = request.tmdbid;
searchSpec.DoubanId = request.doubanid;
searchSpec.RId = request.rid; searchSpec.RId = request.rid;
searchSpec.TvMazeId = request.tvmazeid; searchSpec.TvMazeId = request.tvmazeid;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
} }
@@ -100,6 +105,9 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Author = request.author; searchSpec.Author = request.author;
searchSpec.Title = request.title; searchSpec.Title = request.title;
searchSpec.Publisher = request.publisher;
searchSpec.Year = request.year;
searchSpec.Genre = request.genre;
return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) }; return new NewznabResults { Releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec) };
} }
@@ -152,6 +160,18 @@ namespace NzbDrone.Core.IndexerSearch
.ToList(); .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()); _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)); var tasks = indexers.Select(x => DispatchIndexer(searchAction, x, criteriaBase));
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; 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 */ /* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master"; private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 6; private const int DEFINITION_VERSION = 7;
//Used when moving yml to C# //Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>() private readonly List<string> _defintionBlocklist = new List<string>()
@@ -282,44 +283,29 @@ namespace NzbDrone.Core.IndexerVersions
{ {
var startupFolder = _appFolderInfo.AppDataFolder; 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 try
{ {
EnsureDefinitionsFolder(); 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 archive.ExtractToDirectory(definitionsFolder, true);
{
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);
}
} }
_diskProvider.DeleteFile(saveFile);
_cache.Clear();
_logger.Debug("Updated indexer definitions");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder); _logger.Error(ex, "Definition update failed");
} }
} }
} }
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -493,7 +494,7 @@ namespace NzbDrone.Core.Indexers.Definitions
// Throw common http errors here before we try to parse // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if ((int)releaseResponse.HttpResponse.StatusCode == 429) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
} }
@@ -321,9 +321,10 @@ namespace NzbDrone.Core.Indexers.Definitions
if (episode != null) if (episode != null)
{ {
releaseInfo = episode is > 0 and < 10 var episodeString = episode is > 0 and < 10
? "0" + episode ? "0" + episode
: episode.ToString(); : episode.ToString();
releaseInfo = $" - {episodeString}";
} }
else else
{ {
@@ -449,6 +450,37 @@ namespace NzbDrone.Core.Indexers.Definitions
// Additional 5 hours per GB // Additional 5 hours per GB
minimumSeedTime += (int)((size / 1000000000) * 18000); 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) foreach (var title in synonyms)
{ {
var releaseTitle = groupName == "Movie" ? var releaseTitle = groupName == "Movie" ?
@@ -510,6 +542,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Passkey = ""; Passkey = "";
Username = ""; Username = "";
EnableSonarrCompatibility = true; EnableSonarrCompatibility = true;
UseFilenameForSingleEpisodes = false;
} }
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] [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")] [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; } 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() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -678,10 +714,22 @@ namespace NzbDrone.Core.Indexers.Definitions
[JsonProperty("FileCount")] [JsonProperty("FileCount")]
public int FileCount { get; set; } public int FileCount { get; set; }
[JsonProperty("FileList")]
public List<File> Files { get; set; }
[JsonProperty("UploadTime")] [JsonProperty("UploadTime")]
public DateTimeOffset UploadTime { get; set; } 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 public class EditionData
{ {
[JsonProperty("EditionTitle")] [JsonProperty("EditionTitle")]
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AngleSharp.Html.Parser; using AngleSharp.Html.Parser;
@@ -307,7 +308,7 @@ namespace NzbDrone.Core.Indexers.Definitions
// Throw common http errors here before we try to parse // Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError) if (releaseResponse.HttpResponse.HasHttpError)
{ {
if ((int)releaseResponse.HttpResponse.StatusCode == 429) if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse); throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
} }
@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
[Obsolete("Moved to YML for Cardigann")]
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings> public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
{ {
public override string Name => "Anthelion"; public override string Name => "Anthelion";
@@ -36,11 +36,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
TvSearchParams = new List<TvSearchParam> 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> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.Genre
} }
}; };
@@ -40,6 +40,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[JsonProperty(PropertyName = "video_quality")] [JsonProperty(PropertyName = "video_quality")]
public string VideoQuality { get; set; } public string VideoQuality { get; set; }
public string Type { get; set; } public string Type { get; set; }
public List<AvistazLanguage> Audio { get; set; }
public List<AvistazLanguage> Subtitle { get; set; }
}
public class AvistazLanguage
{
public int Id { get; set; }
public string Language { get; set; }
} }
public class AvistazResponse public class AvistazResponse
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return torrentInfos.ToArray(); return torrentInfos.ToArray();
} }
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
}
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{ {
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request"); 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, UploadVolumeFactor = row.UploadMultiply,
MinimumRatio = 1, MinimumRatio = 1,
MinimumSeedTime = 172800, // 48 hours MinimumSeedTime = 172800, // 48 hours
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
}; };
if (row.MovieTvinfo != null) if (row.MovieTvinfo != null)
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
// hook to adjust the search category // 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 categoryMapping = Capabilities.Categories.MapTorznabCapsToTrackers(categories).Distinct().ToList();
var qc = new List<KeyValuePair<string, string>> // NameValueCollection don't support cat[]=19&cat[]=6 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" } { "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 // resolution filter to improve the search
if (!categories.Contains(NewznabStandardCategory.Movies.Id) && !categories.Contains(NewznabStandardCategory.TV.Id) && if (!categories.Contains(NewznabStandardCategory.Movies.Id) && !categories.Contains(NewznabStandardCategory.TV.Id) &&
!categories.Contains(NewznabStandardCategory.Audio.Id)) !categories.Contains(NewznabStandardCategory.Audio.Id))
@@ -71,7 +81,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{ {
var parameters = GetBasicSearchParameters(searchCriteria.Categories); var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace()) if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{ {
@@ -93,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{ {
var parameters = GetBasicSearchParameters(searchCriteria.Categories); var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim()); parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
@@ -104,7 +114,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{ {
var parameters = GetBasicSearchParameters(searchCriteria.Categories); var parameters = GetBasicSearchParameters(searchCriteria.Categories, searchCriteria.Genre);
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace()) if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{ {
@@ -136,7 +146,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{ {
var parameters = GetBasicSearchParameters(searchCriteria.Categories); var parameters = GetBasicSearchParameters(searchCriteria.Categories, null);
parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim()); parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim());
@@ -22,6 +22,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public AvistazSettings() public AvistazSettings()
{ {
Token = ""; Token = "";
FreeleechOnly = false;
} }
public string Token { get; set; } 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")] [FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
public string Pid { get; set; } 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() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -111,7 +111,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new HttpRequest(searchUrl, HttpAccept.Json); var request = new HttpRequest(searchUrl, HttpAccept.Json);
request.Headers.Add("Content-type", "application/json"); request.Headers.ContentType = "application/json";
request.Method = HttpMethod.Post; request.Method = HttpMethod.Post;
request.SetContent(body.ToJson()); request.SetContent(body.ToJson());
@@ -194,7 +194,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
throw new ReleaseUnavailableException("Downloading torrent failed", ex); throw new ReleaseUnavailableException("Downloading torrent failed", ex);
} }
if ((int)ex.Response.StatusCode == 429) if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
_logger.Error("API Grab Limit reached for {0}", request.Url.FullUri); _logger.Error("API Grab Limit reached for {0}", request.Url.FullUri);
} }
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories(); protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories();
protected readonly List<string> _defaultCategories = new List<string>(); protected readonly List<string> _defaultCategories = new List<string>();
protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "rageid", "tmdbid", "tvdbid", "poster", "banner", "description", "doubanid" }; protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "tmdbid", "rageid", "tvdbid", "tvmazeid", "traktid", "doubanid", "poster", "banner", "description", "genre" };
protected static readonly string[] _SupportedLogicFunctions = protected static readonly string[] _SupportedLogicFunctions =
{ {
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.ContainsIgnoreCase("login.php")) .ContainsIgnoreCase("login.php"))
{ {
CookiesUpdater(null, null); 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"); 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)); throw new CardigannException(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value ?? "<null>", ex.Message));
} }
}
var filters = search.Rows.Filters; var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row); var skipRelease = ParseRowFilters(filters, release, variables, row);
if (skipRelease) if (skipRelease)
{ {
continue; continue;
}
} }
releases.Add(release); releases.Add(release);
@@ -279,7 +279,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
} }
var filters = search.Rows.Filters; var filters = search.Rows.Filters;
var skipRelease = ParseRowFilters(filters, release, variables, row); var skipRelease = ParseRowFilters(filters, release, variables, row.ToHtmlPretty());
if (skipRelease) if (skipRelease)
{ {
@@ -594,8 +594,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.PosterUrl; value = release.PosterUrl;
break; break;
case "genre": case "genre":
release.Genres = release.Genres.Union(value.Split(',')).ToList(); char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
value = release.Genres.ToString(); release.Genres = release.Genres.Union(value.Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries)).ToList();
value = string.Join(", ", release.Genres);
break; break;
case "year": case "year":
release.Year = ParseUtil.CoerceInt(value); release.Year = ParseUtil.CoerceInt(value);
@@ -607,12 +608,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
case "booktitle": case "booktitle":
release.BookTitle = value; release.BookTitle = value;
break; break;
case "publisher":
release.Publisher = value;
break;
case "artist": case "artist":
release.Artist = value; release.Artist = value;
break; break;
case "album": case "album":
release.Album = value; release.Album = value;
break; break;
case "label":
release.Label = value;
break;
case "track":
release.Track = value;
break;
default: default:
break; break;
} }
@@ -644,6 +654,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
// for debugging // for debugging
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString())); _logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString()));
break; break;
case "validate":
char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
var args = (string)filter.Args;
var argsList = args.ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
var validList = argsList.ToList();
var validIntersect = validList.Intersect(row.ToString().ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries)).ToList();
row = string.Join(", ", validIntersect);
break;
default: default:
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name)); _logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
break; break;
@@ -49,10 +49,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Movie"] = null; variables[".Query.Movie"] = null;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null; variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.IMDBID"] = searchCriteria.FullImdbId; variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId; variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null; variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null; variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -70,7 +72,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Album"] = searchCriteria.Album; variables[".Query.Album"] = searchCriteria.Album;
variables[".Query.Artist"] = searchCriteria.Artist; variables[".Query.Artist"] = searchCriteria.Artist;
variables[".Query.Label"] = searchCriteria.Label; variables[".Query.Label"] = searchCriteria.Label;
variables[".Query.Track"] = null; variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.Track"] = searchCriteria.Track;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -88,6 +92,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Series"] = null; variables[".Query.Series"] = null;
variables[".Query.Ep"] = searchCriteria.Episode; variables[".Query.Ep"] = searchCriteria.Episode;
variables[".Query.Season"] = searchCriteria.Season?.ToString() ?? null; variables[".Query.Season"] = searchCriteria.Season?.ToString() ?? null;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
variables[".Query.IMDBID"] = searchCriteria.FullImdbId; variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId; variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TVDBID"] = searchCriteria.TvdbId?.ToString() ?? null; variables[".Query.TVDBID"] = searchCriteria.TvdbId?.ToString() ?? null;
@@ -95,6 +101,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.TVRageID"] = searchCriteria.RId?.ToString() ?? null; variables[".Query.TVRageID"] = searchCriteria.RId?.ToString() ?? null;
variables[".Query.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null; variables[".Query.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null; variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
variables[".Query.DoubanID"] = searchCriteria.DoubanId?.ToString() ?? null;
variables[".Query.Episode"] = searchCriteria.EpisodeSearchString; variables[".Query.Episode"] = searchCriteria.EpisodeSearchString;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -112,6 +119,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Author"] = searchCriteria.Author; variables[".Query.Author"] = searchCriteria.Author;
variables[".Query.Title"] = searchCriteria.Title; variables[".Query.Title"] = searchCriteria.Title;
variables[".Query.Genre"] = searchCriteria.Genre;
variables[".Query.Publisher"] = searchCriteria.Publisher;
variables[".Query.Year"] = searchCriteria.Year?.ToString() ?? null;
pageableRequests.Add(GetRequest(variables)); pageableRequests.Add(GetRequest(variables));
@@ -142,6 +152,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.Offset"] = searchCriteria.Offset?.ToString() ?? null; variables[".Query.Offset"] = searchCriteria.Offset?.ToString() ?? null;
variables[".Query.Extended"] = null; variables[".Query.Extended"] = null;
variables[".Query.APIKey"] = null; variables[".Query.APIKey"] = null;
variables[".Query.Genre"] = null;
//Movie //Movie
variables[".Query.Movie"] = null; variables[".Query.Movie"] = null;
@@ -158,6 +169,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.TVRageID"] = null; variables[".Query.TVRageID"] = null;
variables[".Query.TVMazeID"] = null; variables[".Query.TVMazeID"] = null;
variables[".Query.TraktID"] = null; variables[".Query.TraktID"] = null;
variables[".Query.DoubanID"] = null;
variables[".Query.Episode"] = null; variables[".Query.Episode"] = null;
//Music //Music
@@ -169,6 +181,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
//Book //Book
variables[".Query.Author"] = null; variables[".Query.Author"] = null;
variables[".Query.Title"] = null; variables[".Query.Title"] = null;
variables[".Query.Publisher"] = null;
return variables; return variables;
} }
@@ -343,6 +356,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding Encoding = _encoding
}; };
requestBuilder.SetCookies(Cookies);
requestBuilder.Headers.Add("Referer", loginUrl); requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition); var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
@@ -930,6 +945,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
public bool CheckIfLoginIsNeeded(HttpResponse response) public bool CheckIfLoginIsNeeded(HttpResponse response)
{ {
if (_definition.Login == null)
{
return false;
}
if (response.HasHttpRedirect) if (response.HasHttpRedirect)
{ {
var domainHint = GetRedirectDomainHint(response); 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 + "."; var errormessage = "Got redirected to another domain. Try changing the indexer URL to " + domainHint + ".";
throw new CardigannException(errormessage); _logger.Warn(errormessage);
} }
return true; return true;
} }
if (_definition.Login == null || _definition.Login.Test == null)
{
return false;
}
if (response.HasHttpError) if (response.HasHttpError)
{ {
return true; return true;
} }
// Only run html test selector on html responses // 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 parser = new HtmlParser();
var document = parser.ParseDocument(response.Content); 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); return true;
if (selection.Length == 0)
{
return true;
}
} }
} }
@@ -1109,6 +1121,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath); var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
yield return request; yield return request;
} }
} }
@@ -36,11 +36,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{ {
TvSearchParams = new List<TvSearchParam> 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> MovieSearchParams = new List<MovieSearchParam>
{ {
MovieSearchParam.Q, MovieSearchParam.ImdbId MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.Genre
} }
}; };
@@ -80,6 +80,30 @@ namespace NzbDrone.Core.Indexers.Gazelle
_logger.Debug("Gazelle authentication succeeded."); _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) protected override bool CheckIfLoginNeeded(HttpResponse response)
{ {
if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\""))) 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