Compare commits

...

185 Commits

Author SHA1 Message Date
Erik Persson
b371f2d913 New: Added setting to not include animebytes synonyms 2022-12-08 17:54:57 -06:00
Qstick
3ff3452e2d Handling for Obsolete API Endpoints 2022-12-08 17:50:18 -06:00
Qstick
df13537e29 Fixed: Use route Id for PUT requests if not passed in body 2022-12-08 17:48:33 -06:00
Qstick
5d2fefde8f Fixed: Correct Attribute compare for Id validation 2022-12-08 17:46:49 -06:00
Qstick
ffb3f83324 Simplify X-Forwarded-For handling
This happens in asp.net middleware now

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-12-08 17:45:14 -06:00
Qstick
1c125733b2 New: Improve IPAddress.IsLocal method
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-12-08 17:45:04 -06:00
Mark McDowall
2af7fac15e New: IPv6 support for connections/indexers/download clients 2022-12-08 17:44:24 -06:00
Mark McDowall
f172d17ecc Fixed: Improve Bind Address validation and help text 2022-12-08 17:42:55 -06:00
Zak Saunders
c69843931e New: Auto theme option to match OS theme
Co-authored-by: Qstick <qstick@gmail.com>
2022-12-08 17:40:59 -06:00
bakerboy448
cd3e99ad87 Fixed: Indexer Error handling improvements (#1172)
* Fixed: Indexer Error handling improvements

* fixup! Fixed: Indexer Error handling improvements
2022-12-01 21:30:27 -06:00
Qstick
1cce39b404 Fix Orpheus Tests 2022-11-29 20:15:23 -06:00
Qstick
9b46ab73e4 Fixed: (Orpheus) Parse date from epoch or date time string 2022-11-29 19:42:35 -06:00
Mark McDowall
a352c053ab Fixed: Publish ApplicationStartingEvent during startup
(cherry picked from commit 5400bce1295bdc4198d2cfe0b9258bbb7ccf0852)

Fixes #1199
2022-11-26 10:17:05 -06:00
Weblate
b33e45d266 Translated using Weblate (Slovak)
Currently translated at 23.0% (107 of 464 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 23.9% (111 of 464 strings)

Translated using Weblate (Catalan)

Currently translated at 75.6% (351 of 464 strings)

Translated using Weblate (Arabic)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Vietnamese)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Turkish)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (Thai)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Swedish)

Currently translated at 88.1% (409 of 464 strings)

Translated using Weblate (Russian)

Currently translated at 77.5% (360 of 464 strings)

Translated using Weblate (Romanian)

Currently translated at 73.2% (340 of 464 strings)

Translated using Weblate (Portuguese)

Currently translated at 80.8% (375 of 464 strings)

Translated using Weblate (Polish)

Currently translated at 75.6% (351 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 88.7% (412 of 464 strings)

Translated using Weblate (Korean)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Japanese)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Italian)

Currently translated at 98.0% (455 of 464 strings)

Translated using Weblate (Icelandic)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Hindi)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Hebrew)

Currently translated at 74.5% (346 of 464 strings)

Translated using Weblate (French)

Currently translated at 96.7% (449 of 464 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Spanish)

Currently translated at 79.3% (368 of 464 strings)

Translated using Weblate (Greek)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Danish)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (Czech)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Czech)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Bulgarian)

Currently translated at 68.3% (317 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 99.3% (461 of 464 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Don-Chris <Chr_Sch@t-online.de>
Co-authored-by: Tordai, Ralph <ralph_t@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translation: Servarr/Prowlarr
2022-11-22 19:58:33 -06:00
bakerboy448
817d61de91 Fixed: (SpeedApp) Migrate Legacy URL without slash 2022-11-22 19:57:18 -06:00
Rumplin
c7e5cc6462 Removed suspicious URL in the default definitions (#1208)
* Removed suspicious URL in the default definitions

Revert "Removed suspicious URL in the default definitions"

This reverts commit e26853f9aa919cd413b0f8b914ac426f220b9475.

* Update Torznab.cs

Removed suspicious URL from the code. Looks like the site that was originally there doesn't exist anymore and it's hosting malware (HD4Free.xyz).

Co-authored-by: admin <stanislav.ivanov@performit.ie>
2022-11-22 19:56:48 -06:00
Qstick
25596fc2e8 Fixed: Orpheus migration fails on Postgres 2022-11-19 13:59:31 -05:00
Qstick
9ff0b90626 Convert Notifiarr Payload to JSON, Standardize with Webhook (#1194)
* Convert Notifiarr Payload to JSON, Standardize with Webhook

* fixup!
2022-11-13 14:18:44 -05:00
ta264
4f4c011436 Swap Orpheus to API key (#946)
* New: Orpheus uses API key instead of user/pass

* fixup! New: Orpheus uses API key instead of user/pass

Co-authored-by: Qstick <qstick@gmail.com>
2022-11-13 11:07:55 -06:00
Qstick
bd0115931f Bump version to 0.4.10 2022-11-12 22:02:13 -06:00
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
Qstick
cec304a0be Don't require user agent for IPTorrents 2022-07-04 18:13:18 -05:00
Qstick
06f3c8e151 Fixed: (Applications) ApiPath can be null from -arr in some cases 2022-07-04 14:24:32 -05:00
Qstick
91eb65bd6c ProtectionService Test Fixture 2022-07-04 13:02:25 -05:00
Qstick
832080cb36 Fixed: Lidarr null ref when building indexer for sync
Fixes PROWLARR-B5Y
2022-07-04 12:59:58 -05:00
Qstick
f9c731627f Fixed: Lidarr null ref when building indexer for sync
Fixes PROWLARR-856
2022-07-04 12:58:50 -05:00
Qstick
83344fb6f4 Double MultipartBodyLengthLimit for Backup Restore to 256MB 2022-07-04 11:21:14 -05:00
Qstick
59b7435820 Fixed: (IPTorrents) Allow UA override for CF
Fixes #842
Fixes #809
2022-07-03 17:23:47 -05:00
bakerboy448
d2c1ffa761 Fixed: Log Cleanse Indexer Response Logic and Test Cases 2022-07-03 15:05:33 -05:00
Qstick
5436d4f800 Fixed: Set update executable permissions correctly 2022-07-03 12:32:41 -05:00
Qstick
86fe19a5dd Fixed: Don't call for server notifications on event driven check
[common]
2022-07-03 12:26:02 -05:00
Qstick
473405ceeb Update file and folder handling methods from Radarr (#1051)
* Update file/folder handling methods from Radarr

* fixup!
2022-07-02 18:40:00 -05:00
Robin Dadswell
cac2729230 Running Integration Tests against Postgres Database (#838)
* Allow configuring postgres with environment variables

(cherry picked from commit 8439df78fea25656a9a1275d2a2fe3f0df0528c7)

* Fix process provider when environment variables alread exist

[common]

(cherry picked from commit 66e5b4025974e081c1406f01a860b1ac52949c22)

* First bash at running integration tests on postgres

(cherry picked from commit f950e80c7e4f9b088ec6a149386160eab83b61c3)

* Postgres integration tests running as part of the build pipeline

(cherry picked from commit 9ca8616f5098778e9b5e6ce09d2aa11224018fab)

* Fixed: Register PostgresOptions when running in utility mode

* fixup!

* fixup!

* fixup!

* fixup!

* fixup!

Co-authored-by: ta264 <ta264@users.noreply.github.com>
Co-authored-by: Qstick <qstick@gmail.com>
2022-07-02 17:48:10 -05:00
Robin Dadswell
9b1f9abfac Updated NLog Version (#7365) 2022-07-02 14:22:23 -05:00
Qstick
76285a8ccd Add additional link logging to DownloadService 2022-06-28 19:45:16 -05:00
Qstick
a6b499e4a5 Fixed: Correctly remove TorrentParadiseMl
Fixes #1046
2022-06-28 18:31:13 -05:00
Qstick
5ee95e3cc2 V6 Cardigann Changes (#1045)
* V6 Cardigann Changes

* fixup!

* !fixup range

* !fixup more cardigann tests
2022-06-27 20:39:15 -05:00
Mark McDowall
654d2dbad3 Sliding expiration for auth cookie and a little clean up 2022-06-26 11:19:10 -05:00
Qstick
67ae7e32df Bump version to 0.4.2 2022-06-26 10:50:01 -05:00
Qstick
d52516b700 Update Sentry to 3.18.0 2022-06-25 18:26:37 -05:00
Qstick
326a7b5e16 Update Swashbuckle to 6.3.1 2022-06-25 18:26:07 -05:00
Qstick
313a0b459a Bump dotnet to 6.0.6 2022-06-25 18:24:44 -05:00
Qstick
2ffe88bf6a Update AngleSharp to 0.17.0 2022-06-25 18:22:28 -05:00
Qstick
a311d13805 Remove ShowRSS C# Implementation 2022-06-25 18:06:40 -05:00
Qstick
0e2d15cb73 Swallow HTTP issues on analytics call 2022-06-25 16:19:55 -05:00
Qstick
d1949d24e0 Fix NullRef in analytics service 2022-06-25 16:12:50 -05:00
Qstick
cdb1f163f8 Bump version to 0.4.1 2022-06-25 13:05:54 -05:00
Qstick
580fc528e5 Fix Donation Links 2022-06-24 18:49:08 -05:00
Qstick
dfed229a1d Fix Tooltips in Dark Theme 2022-06-24 18:46:58 -05:00
bakerboy448
e76a255229 Fixed: (AnimeBytes) Cleanse Passkey from response
Fixes #1041
2022-06-24 09:54:36 -05:00
Qstick
a0b650e7a5 Fixed: (Cardigann) Use variables in keywordsfilters block
Fixes #1035
Fixes v5 TorrentLand
2022-06-23 22:22:30 -05:00
Qstick
7cf9fc6a4f New: (BeyondHD) Better status messages for failures
Closes #1028
2022-06-23 20:56:07 -05:00
Qstick
86f5768461 Fixed: VIP Healthcheck not triggered for expired indexers 2022-06-23 20:36:13 -05:00
ta264
479e29dde7 Use DryIoc for Automoqer, drop Unity dependency
(cherry picked from commit e3468daba04b52fbf41ce3004934a26b0220ec4f)
2022-06-22 10:57:36 +01:00
olly
761e15a476 New: Send description element in nab response 2022-06-21 09:16:07 -05:00
Davo1624
d3ffb7b490 (Filelist) Update help text for pass key (#1039) 2022-06-21 09:14:02 -05:00
Qstick
0db804b647 Fixed: (Exoticaz) Category parsing kills search/feed
Fixes #938
2022-06-20 21:39:20 -05:00
Qstick
4334e7eef1 New: (PassThePopcorn) Freeleech only option
Fixes #1014
2022-06-11 15:04:35 -05:00
Qstick
fbfb70a1bb Fixed: (Cardigann) Searching with nab Parent should also use Child categories
Fixes #1031
2022-06-11 14:22:09 -05:00
bakerboy448
59e990227d Fixed: Better Cleansing of Tracker Announce Keys
Fixed: Cleanse Notifiarr secret from URL in logs

(cherry picked from commit e6210aede6f7ead197e82572976bc0267d910d46)
(cherry picked from commit ec866082d44d299096112a6c7c232384b1f74505)
2022-06-11 13:42:32 -05:00
Servarr
f0abfae978 Automated API Docs update 2022-06-04 08:47:47 -05:00
Qstick
40e7f80be9 Update FE dev dependencies 2022-06-04 00:42:40 -05:00
ta264
3c913eac24 Ensure .Mono and .Windows projects have all dependencies in build output
Fixes development on linux
2022-05-31 05:35:16 +01:00
Qstick
95a2bd3d03 Fixed: (Gazelle) Parse grouptime as long or date
Closes #973
Fixes #773
Closes #1008
2022-05-19 21:58:35 -05:00
Qstick
d5abde98e1 Fixed: (ExoticaZ) Category Parsing
Fixes #938
2022-05-19 21:23:33 -05:00
Qstick
5d14d2c134 Fixed: Input options background color on mobile 2022-05-18 15:47:17 -04:00
gofaster
ed46c5c86d Fixed: Update AltHub API URL (#1010) 2022-05-18 12:36:16 -05:00
316 changed files with 69443 additions and 5297 deletions

View File

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

View File

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

132
CODE_OF_CONDUCT.md Normal file
View File

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

View File

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

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.0'
majorVersion: '0.4.10'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.201'
dotnetVersion: '6.0.301'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'
@@ -97,15 +97,14 @@ stages:
- bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
echo "Extra platforms already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
displayName: Enable FreeBSD Support
- bash: ./build.sh --backend --enable-bsd
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Prowlarr Backend
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -119,24 +118,28 @@ stages:
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests
displayName: Publish MacOS Test Package
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend
@@ -239,35 +242,35 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd
- bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages
- bash: |
find . -name "Prowlarr" -exec chmod a+x {} \;
find . -name "Prowlarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits
- task: ArchiveFiles@2
displayName: Create Windows Core zip
displayName: Create win-x64 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Windows x86 Core zip
displayName: Create win-x86 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS x64 Core app
displayName: Create osx-x64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar
displayName: Create osx-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
@@ -275,14 +278,14 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app
displayName: Create osx-arm64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar
displayName: Create osx-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
@@ -290,7 +293,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Core tar
displayName: Create linux-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar'
@@ -298,7 +301,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Musl Core tar
displayName: Create linux-musl-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar'
@@ -306,7 +309,15 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar
displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar'
@@ -314,7 +325,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar
displayName: Create linux-musl-arm tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
@@ -322,7 +333,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar
displayName: Create linux-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar'
@@ -330,7 +341,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar
displayName: Create linux-musl-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar'
@@ -405,22 +416,22 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'MacCore'
testName: 'osx-x64'
poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }}
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
testName: 'win-x64'
poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }}
LinuxCore:
osName: 'Linux'
testName: 'LinuxCore'
testName: 'linux-x64'
poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }}
FreebsdCore:
osName: 'Linux'
testName: 'FreebsdCore'
testName: 'freebsd-x64'
poolName: 'FreeBSD'
imageName:
@@ -439,7 +450,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)Tests'
artifactName: '$(testName)-tests'
targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
@@ -469,8 +480,12 @@ stages:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: ${{ variables.linuxImage }}
@@ -481,9 +496,15 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -506,6 +527,58 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
displayName: Integration
@@ -533,17 +606,17 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'MacCore'
testName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
testName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip'
LinuxCore:
osName: 'Linux'
testName: 'LinuxCore'
testName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
@@ -560,7 +633,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)Tests'
artifactName: '$(testName)-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -590,6 +663,67 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
dependsOn: Prepare
@@ -607,14 +741,14 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'FreebsdCoreTests'
artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '/$(pattern)'
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -643,11 +777,15 @@ stages:
strategy:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
testName: 'linux-musl-x64'
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
pool:
vmImage: ${{ variables.linuxImage }}
@@ -657,9 +795,15 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -705,16 +849,19 @@ stages:
matrix:
Linux:
osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac:
osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows:
osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: true
@@ -732,7 +879,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(osName)CoreTests'
artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -961,4 +1108,5 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)

View File

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

View File

@@ -78,7 +78,7 @@
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
background-color: var(--white);
background-color: var(--inputBackgroundColor);
}
.loading {

View File

@@ -74,7 +74,7 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
to="https://opencollective.com/prowlarr"
to="https://prowlarr.com/donate"
size={14}
/>
<IconButton

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
position: relative;
&.default {
background-color: var(--white);
background-color: var(--popoverBodyBackgroundColor);
box-shadow: 0 5px 10px var(--popoverShadowColor);
}

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ const searchOptions = [
const seriesTokens = [
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
{ token: '{TvdbId:12345}', example: '12345' },
{ token: '{TmdbId:12345}', example: '12345' },
{ token: '{TvMazeId:12345}', example: '54321' },
{ token: '{Season:00}', example: '01' },
{ token: '{Episode:00}', example: '01' }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,10 +168,11 @@ module.exports = {
//
// Popover
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverTitleBackgroundColor: '#424242',
popoverTitleBorderColor: '#2a2a2a',
popoverBodyBackgroundColor: '#2a2a2a',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#fff',
popoverArrowBorderColor: '#2a2a2a',
popoverTitleBackgroundInverseColor: '#595959',
popoverTitleBorderInverseColor: '#707070',

View File

@@ -1,7 +1,11 @@
import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
export default {
auto,
light,
dark
};

View File

@@ -170,6 +170,7 @@ module.exports = {
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverBodyBackgroundColor: '#e9e9e9',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#fff',

View File

@@ -13,7 +13,7 @@ class Donations extends Component {
return (
<FieldSet legend={translate('Donations')}>
<div className={styles.logoContainer} title="Radarr">
<Link to="https://opencollective.com/radarr">
<Link to="https://radarr.video/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
@@ -21,7 +21,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Lidarr">
<Link to="https://opencollective.com/lidarr">
<Link to="https://lidarr.audio/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
@@ -29,7 +29,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://opencollective.com/readarr">
<Link to="https://readarr.com/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
@@ -37,7 +37,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://opencollective.com/prowlarr">
<Link to="https://prowlarr.com/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}

View File

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

View File

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

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.3",
"@microsoft/signalr": "6.0.6",
"@sentry/browser": "6.19.2",
"@sentry/integrations": "6.19.2",
"chart.js": "3.7.1",
@@ -78,38 +78,38 @@
"reselect": "4.0.0"
},
"devDependencies": {
"@babel/core": "7.17.8",
"@babel/eslint-parser": "7.17.0",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-decorators": "7.17.8",
"@babel/plugin-proposal-export-default-from": "7.16.7",
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
"@babel/plugin-proposal-function-sent": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/core": "7.18.2",
"@babel/eslint-parser": "7.18.2",
"@babel/plugin-proposal-class-properties": "7.17.12",
"@babel/plugin-proposal-decorators": "7.18.2",
"@babel/plugin-proposal-export-default-from": "7.17.12",
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
"@babel/plugin-proposal-function-sent": "7.18.2",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@babel/plugin-proposal-throw-expressions": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"autoprefixer": "10.4.4",
"babel-loader": "8.2.4",
"@babel/preset-env": "7.18.2",
"@babel/preset-react": "7.17.12",
"autoprefixer": "10.4.7",
"babel-loader": "8.2.5",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.21.1",
"core-js": "3.22.8",
"css-loader": "6.7.1",
"eslint": "8.11.0",
"eslint": "8.17.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.30.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.3.0",
"esprint": "3.6.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0",
"mini-css-extract-plugin": "2.6.0",
"postcss": "8.4.12",
"postcss": "8.4.14",
"postcss-color-function": "4.1.0",
"postcss-loader": "6.2.1",
"postcss-mixins": "9.0.2",
@@ -121,10 +121,10 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"stylelint": "14.6.0",
"stylelint": "14.8.5",
"stylelint-order": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.70.0",
"webpack": "5.73.0",
"webpack-cli": "4.9.2",
"webpack-livereload-plugin": "3.0.2"
}

View File

@@ -94,7 +94,7 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />

View File

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

View File

@@ -10,6 +10,16 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider
{
[Test]
public void writealltext_should_truncate_existing()
{
var file = GetTempFilePath();
Subject.WriteAllText(file, "A pretty long string");
Subject.WriteAllText(file, "A short string");
Subject.ReadAllText(file).Should().Be("A short string");
}
[Test]
[Retry(5)]
public void directory_exist_should_be_able_to_find_existing_folder()

View File

@@ -402,6 +402,40 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(source.FullName, destination.FullName);
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_ignore_nfs_temp_file()
{
@@ -451,6 +485,42 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName);
}
[Test]
public void MoveFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move));
}
[Test]
public void MoveFolder_should_rename_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
source.FullName.GetActualCasing().Should().Be(destination.FullName);
}
[Test]
public void should_throw_if_destination_is_readonly()
{
@@ -553,6 +623,23 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void MirrorFolder_should_handle_trailing_slash()
{
WithRealDiskProvider();
var original = GetFilledTempFolder();
var source = new DirectoryInfo(GetTempFilePath());
var destination = new DirectoryInfo(GetTempFilePath());
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName);
count.Should().Equals(3);
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void TransferFolder_should_use_movefolder_if_on_same_mount()
{
@@ -752,6 +839,10 @@ namespace NzbDrone.Common.Test.DiskTests
.Setup(v => v.CreateFolder(It.IsAny<string>()))
.Callback<string>(v => Directory.CreateDirectory(v));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFolder(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, string, bool>((v, r, b) => Directory.Move(v, r));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, bool>((v, r) => Directory.Delete(v, r));

View File

@@ -28,14 +28,6 @@ namespace NzbDrone.Common.Test.DiskTests
Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_check_space_on_ramdrive()
{
PosixOnly();
Subject.GetAvailableSpace("/run/").Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_return_free_disk_space()
@@ -44,35 +36,6 @@ namespace NzbDrone.Common.Test.DiskTests
result.Should().BeGreaterThan(0);
}
[Test]
public void should_be_able_to_get_space_on_unc()
{
WindowsOnly();
var result = Subject.GetAvailableSpace(@"\\localhost\c$\Windows");
result.Should().BeGreaterThan(0);
}
[Test]
public void should_throw_if_drive_doesnt_exist()
{
WindowsOnly();
// Find a drive that doesn't exist.
for (char driveletter = 'Z'; driveletter > 'D'; driveletter--)
{
if (new DriveInfo(driveletter.ToString()).IsReady)
{
continue;
}
Assert.Throws<DirectoryNotFoundException>(() => Subject.GetAvailableSpace(driveletter + @":\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic()));
return;
}
Assert.Inconclusive("No drive available for testing.");
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_get_space_on_folder_that_doesnt_exist()

View File

@@ -0,0 +1,25 @@
using System.Globalization;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
{
[TestFixture]
public class IsValidIPAddressFixture
{
[TestCase("192.168.0.1")]
[TestCase("::1")]
[TestCase("2001:db8:4006:812::200e")]
public void should_validate_ip_address(string input)
{
input.IsValidIpAddress().Should().BeTrue();
}
[TestCase("sonarr.tv")]
public void should_not_parse_non_ip_address(string input)
{
input.IsValidIpAddress().Should().BeFalse();
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Test.Common;
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("abc://my_host.com:8080/root/api/")]
[TestCase("abc://my_host.com:8080//root/api/")]
[TestCase("abc://my_host.com:8080/root//api/")]
[TestCase("abc://[::1]:8080/root//api/")]
public void should_parse(string uri)
{
var newUri = new HttpUri(uri);

View File

@@ -28,9 +28,12 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Indexer and Download Client Responses
// avistaz response
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
// animebytes response
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
// danish bytes response
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
@@ -77,29 +80,48 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Download Station
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
// Tracker Responses
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// BroadcastheNet
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
[TestCase(@"""DownloadURL"":""https://broadcasthe.net/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
// Notifiarr
// Webhooks - Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/prowlarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
// RSS
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&amp;extended=1&amp;cat=3030&apikey=mySecret&amp;q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
// Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
}
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
[TestCase(@"Some message (from 32.2.3.5 user agent)")]
[TestCase(@"Auth-Invalidated ip 32.2.3.5")]
[TestCase(@"Auth-Success ip 32.2.3.5")]

View File

@@ -170,7 +170,7 @@ namespace NzbDrone.Common.Test
var processStarted = new ManualResetEventSlim();
string suffix;
if (OsInfo.IsWindows || PlatformInfo.IsMono)
if (OsInfo.IsWindows)
{
suffix = ".exe";
}

View File

@@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@@ -30,7 +30,8 @@ namespace NzbDrone.Common.Disk
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetEveryonePermissions(string filename);
public abstract void SetPermissions(string path, string mask);
public abstract void SetFilePermissions(string path, string mask, string group);
public abstract void SetPermissions(string path, string mask, string group);
public abstract void CopyPermissions(string sourcePath, string targetPath);
public abstract long? GetTotalSize(string path);
@@ -130,7 +131,7 @@ namespace NzbDrone.Common.Disk
{
var testPath = Path.Combine(path, "prowlarr_write_test.txt");
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
File.WriteAllText(testPath, testContent);
WriteAllText(testPath, testContent);
File.Delete(testPath);
return true;
}
@@ -258,17 +259,6 @@ namespace NzbDrone.Common.Disk
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
if (source.PathEquals(destination))
{
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
}
if (FolderExists(destination) && overwrite)
{
DeleteFolder(destination, true);
}
RemoveReadOnlyFolder(source);
Directory.Move(source, destination);
}
@@ -310,7 +300,16 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(filename, () => filename).IsValidPath();
RemoveReadOnly(filename);
File.WriteAllText(filename, contents);
// File.WriteAllText is broken on net core when writing to some CIFS mounts
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var writer = new StreamWriter(fs))
{
writer.Write(contents);
}
}
}
public void FolderSetLastWriteTime(string path, DateTime dateTime)
@@ -550,7 +549,7 @@ namespace NzbDrone.Common.Disk
}
}
public virtual bool IsValidFilePermissionMask(string mask)
public virtual bool IsValidFolderPermissionMask(string mask)
{
throw new NotSupportedException();
}

View File

@@ -4,7 +4,6 @@ using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Disk
@@ -27,11 +26,56 @@ namespace NzbDrone.Common.Disk
_logger = logger;
}
private string ResolveRealParentPath(string path)
{
var parentPath = path.GetParentPath();
if (!_diskProvider.FolderExists(parentPath))
{
return path;
}
var realParentPath = parentPath.GetActualCasing();
var partialChildPath = path.Substring(parentPath.Length);
return realParentPath + partialChildPath;
}
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
{
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} Directory [{1}] > [{2}]", mode, sourcePath, targetPath);
if (sourcePath == targetPath)
{
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
}
if (mode == TransferMode.Move && sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase) && _diskProvider.FolderExists(targetPath))
{
// Move folder out of the way to allow case-insensitive renames
var tempPath = sourcePath + ".backup~";
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", sourcePath, tempPath);
_diskProvider.MoveFolder(sourcePath, tempPath);
if (!_diskProvider.FolderExists(targetPath))
{
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, targetPath);
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(tempPath, targetPath);
return mode;
}
// There were two separate folders, revert the intermediate rename and let the recursion deal with it
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, sourcePath);
_diskProvider.MoveFolder(tempPath, sourcePath);
}
if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
{
var sourceMount = _diskProvider.GetMount(sourcePath);
@@ -40,7 +84,7 @@ namespace NzbDrone.Common.Disk
// If we're on the same mount, do a simple folder move.
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory)
{
_logger.Debug("Move Directory [{0}] > [{1}]", sourcePath, targetPath);
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(sourcePath, targetPath);
return mode;
}
@@ -79,6 +123,13 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
var totalSize = _diskProvider.GetFileInfos(sourcePath).Sum(v => v.Length);
if (totalSize > (100 * 1024L * 1024L))
{
throw new IOException($"Large files still exist in {sourcePath} after folder move, not deleting source folder");
}
_diskProvider.DeleteFolder(sourcePath, true);
}
@@ -92,7 +143,10 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
_logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath);
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("Mirror Folder [{0}] > [{1}]", sourcePath, targetPath);
if (!_diskProvider.FolderExists(targetPath))
{
@@ -204,6 +258,9 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
var originalSize = _diskProvider.GetFileSize(sourcePath);

View File

@@ -11,7 +11,8 @@ namespace NzbDrone.Common.Disk
long? GetAvailableSpace(string path);
void InheritFolderPermissions(string filename);
void SetEveryonePermissions(string filename);
void SetPermissions(string path, string mask);
void SetFilePermissions(string path, string mask, string group);
void SetPermissions(string path, string mask, string group);
void CopyPermissions(string sourcePath, string targetPath);
long? GetTotalSize(string path);
DateTime FolderGetCreationTime(string path);
@@ -56,6 +57,6 @@ namespace NzbDrone.Common.Disk
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
bool IsValidFilePermissionMask(string mask);
bool IsValidFolderPermissionMask(string mask);
}
}

View File

@@ -1,15 +1,79 @@
using System;
using System.IO;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Disk
{
public static class LongPathSupport
{
private static int MAX_PATH;
private static int MAX_NAME;
public static void Enable()
{
// Mono has an issue with enabling long path support via app.config.
// This works for both mono and .net on Windows.
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
DetectLongPathLimits();
}
private static void DetectLongPathLimits()
{
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_PATH"), out MAX_PATH))
{
if (OsInfo.IsLinux)
{
MAX_PATH = 4096;
}
else
{
try
{
// Windows paths can be up to 32,767 characters long, but each component of the path must be less than 255.
// If the OS does not have Long Path enabled, then the following will throw an exception
// ref: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
Path.GetDirectoryName($@"C:\{new string('a', 254)}\{new string('a', 254)}");
MAX_PATH = 4096;
}
catch
{
MAX_PATH = 260 - 1;
}
}
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_NAME"), out MAX_NAME))
{
MAX_NAME = 255;
}
}
public static int MaxFilePathLength
{
get
{
if (MAX_PATH == 0)
{
DetectLongPathLimits();
}
return MAX_PATH;
}
}
public static int MaxFileNameLength
{
get
{
if (MAX_NAME == 0)
{
DetectLongPathLimits();
}
return MAX_NAME;
}
}
}
}

View File

@@ -7,34 +7,50 @@ namespace NzbDrone.Common.Extensions
{
public static bool IsLocalAddress(this IPAddress ipAddress)
{
if (ipAddress.IsIPv6LinkLocal)
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
if (ipAddress.IsIPv4MappedToIPv6)
{
return true;
ipAddress = ipAddress.MapToIPv4();
}
// Checks loopback ranges for both IPv4 and IPv6.
if (IPAddress.IsLoopback(ipAddress))
{
return true;
}
// IPv4
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
{
byte[] bytes = ipAddress.GetAddressBytes();
switch (bytes[0])
{
case 10:
case 127:
return true;
case 172:
return bytes[1] < 32 && bytes[1] >= 16;
case 192:
return bytes[1] == 168;
default:
return false;
}
return IsLocalIPv4(ipAddress.GetAddressBytes());
}
// IPv6
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
return ipAddress.IsIPv6LinkLocal ||
ipAddress.IsIPv6UniqueLocal ||
ipAddress.IsIPv6SiteLocal;
}
return false;
}
private static bool IsLocalIPv4(byte[] ipv4Bytes)
{
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
// Class A private range: 10.0.0.0 10.255.255.255 (10.0.0.0/8)
bool IsClassA() => ipv4Bytes[0] == 10;
// Class B private range: 172.16.0.0 172.31.255.255 (172.16.0.0/12)
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
// Class C private range: 192.168.0.0 192.168.255.255 (192.168.0.0/16)
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
@@ -210,5 +211,51 @@ namespace NzbDrone.Common.Extensions
return result.TrimStart(' ', '.').TrimEnd(' ');
}
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
public static bool IsValidIpAddress(this string value)
{
if (!IPAddress.TryParse(value, out var parsedAddress))
{
return false;
}
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
{
return false;
}
if (parsedAddress.IsIPv6Multicast)
{
return false;
}
return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6;
}
public static string ToUrlHost(this string input)
{
return input.Contains(":") ? $"[{input}]" : input;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
{
public class HttpUri : IEquatable<HttpUri>
{
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _uri;
public string FullUri => _uri;
@@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http
private void Parse()
{
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
var match = RegexUri.Match(_uri);
var scheme = match.Groups["scheme"];
@@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http
var query = match.Groups["query"];
var fragment = match.Groups["fragment"];
if (!match.Success || (scheme.Success && !host.Success && path.Success))
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
{
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
}

View File

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

View File

@@ -11,13 +11,14 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -28,6 +29,9 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -51,7 +55,8 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}\\\/rss\\\/download\\\/(?<secret>[^&=]+?)\\\/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Fluent;
@@ -8,47 +10,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return logBuilder.Property("Sentry", fingerprint);
}
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
}
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
}
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
}
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
}
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
{
SentryLogger.Log(level)
.CopyLogEvent(logBuilder.LogEventInfo)
SentryLogger.ForLogEvent(level)
.CopyLogEvent(logBuilder.LogEvent)
.SentryFingerprint(fingerprint)
.Write();
.Log();
return logBuilder.Property("Sentry", null);
return logBuilder.Property<string>("Sentry", null);
}
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
{
return logBuilder.LoggerName(logEvent.LoggerName)
.TimeStamp(logEvent.TimeStamp)
return logBuilder.TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters)
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
.Exception(logEvent.Exception);
}
}

View File

@@ -1,13 +1,16 @@
using NLog;
using System;
using System.Text;
using NLog;
using NLog.Targets;
namespace NzbDrone.Common.Instrumentation
{
public class NzbDroneFileTarget : FileTarget
{
protected override string GetFormattedMessage(LogEventInfo logEvent)
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
target.Append(result);
}
}
}

View File

@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached)
{
RegisterDebugger();
@@ -97,10 +99,21 @@ namespace NzbDrone.Common.Instrumentation
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
var loggingRule = new LoggingRule("*", LogLevel.Trace, target);
LogManager.Configuration.AddTarget("debugger", target);
LogManager.Configuration.LoggingRules.Add(loggingRule);
}
private static void RegisterGlobalFilters()
{
LogManager.Setup().LoadConfiguration(c =>
{
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
});
}
private static void RegisterConsole()
{
var level = LogLevel.Trace;

View File

@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
try
{
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
var key = environmentVariable.Key.ToString();
var value = environmentVariable.Value?.ToString();
if (startInfo.EnvironmentVariables.ContainsKey(key))
{
startInfo.EnvironmentVariables[key] = value;
}
else
{
startInfo.EnvironmentVariables.Add(key, value);
}
}
catch (Exception e)
{

View File

@@ -4,13 +4,14 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="Sentry" Version="3.15.0" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />

View File

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

View File

@@ -0,0 +1,58 @@
using System;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class orpheus_apiFixture : MigrationTest<orpheus_api>
{
[Test]
public void should_convert_and_disable_orpheus_instance()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Enable = true,
Name = "Orpheus",
Priority = 25,
Added = DateTime.UtcNow,
Implementation = "Orpheus",
Settings = new GazelleIndexerSettings021
{
Username = "some name",
Password = "some pass"
}.ToJson(),
ConfigContract = "GazelleSettings"
});
});
var items = db.Query<IndexerDefinition022>("SELECT \"Id\", \"Enable\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().ConfigContract.Should().Be("OrpheusSettings");
items.First().Enable.Should().Be(false);
items.First().Settings.Should().NotContain("username");
items.First().Settings.Should().NotContain("password");
}
}
public class IndexerDefinition022
{
public int Id { get; set; }
public bool Enable { get; set; }
public string ConfigContract { get; set; }
public string Settings { get; set; }
}
public class GazelleIndexerSettings021
{
public string Username { get; set; }
public string Password { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,10 +5,14 @@ using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework
{
@@ -49,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest
{
private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -101,17 +106,39 @@ namespace NzbDrone.Core.Test.Framework
private IDatabase CreateDatabase(MigrationContext migrationContext)
{
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new
if (migrationContext.BeforeMigration != null)
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
{
return factory.Create(migrationContext);
}
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db
var cachedDb = GetCachedDatabase(migrationContext.MigrationType);
var testDb = GetTestDb(migrationContext.MigrationType);
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
if (File.Exists(cachedDb))
{
TestLogger.Info($"Using cached initial database {cachedDb}");
@@ -131,12 +158,7 @@ namespace NzbDrone.Core.Test.Framework
}
}
private string GetCachedDatabase(MigrationType type)
{
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
}
private string GetTestDb(MigrationType type)
private string GetTestSqliteDb(MigrationType type)
{
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
}
@@ -151,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath();
SetupLogging();
// populate the possible postgres options
var postgresOptions = PostgresDatabase.GetTestOptions();
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
// Set up remaining container services
Mocker.SetConstant(Options.Create(postgresOptions));
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@@ -171,11 +200,17 @@ namespace NzbDrone.Core.Test.Framework
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null)
{
DeleteTempFolder(TestFolderInfo.AppDataFolder);
}
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
}
}
}

View File

@@ -1,5 +1,7 @@
using System.IO;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test
{
@@ -10,13 +12,13 @@ namespace NzbDrone.Core.Test
[OneTimeTearDown]
public void ClearCachedDatabase()
{
var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db");
var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main);
if (File.Exists(mainCache))
{
File.Delete(mainCache);
}
var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db");
var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log);
if (File.Exists(logCache))
{
File.Delete(logCache);

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
}
public class TestDatabase : ITestDatabase
@@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection)
{
_eventAggregator = new Mock<IEventAggregator>().Object;

View File

@@ -41,7 +41,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean();
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().Be(expectedTime));
// BeCloseTo handles Postgres rounding times
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime));
}
}
}

View File

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

View File

@@ -0,0 +1,63 @@
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.Indexers.Definitions.Avistaz;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
[TestFixture]
public class ExoticazFixture : CoreTest<ExoticaZ>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "ExoticaZ",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
[Test]
public async Task should_parse_recent_feed_from_ExoticaZ()
{
var recentFeed = ReadAllText(@"Files/Indexers/Exoticaz/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 MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[SSIS-419] My first experience is Yua Mikami. From the day I lost my virginity, I was devoted to sex.");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://exoticaz.to/rss/download/(removed)/(removed).torrent");
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(33);
torrentInfo.Seeders.Should().Be(33);
torrentInfo.Categories.First().Id.Should().Be(6040);
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
{
public class ApplyGoTemplateTextFixture : CoreTest<CardigannBase>
{
private Dictionary<string, object> _variables;
private CardigannDefinition _definition;
[SetUp]
public void SetUp()
{
_variables = new Dictionary<string, object>
{
[".Config.sitelink"] = "https://somesite.com/",
[".True"] = "True",
[".False"] = null,
[".Today.Year"] = DateTime.Today.Year.ToString(),
[".Categories"] = new string[] { "tv", "movies" }
};
_definition = Builder<CardigannDefinition>.CreateNew()
.With(x => x.Encoding = "UTF-8")
.With(x => x.Links = new List<string>
{
"https://somesite.com/"
})
.With(x => x.Caps = new CapabilitiesBlock
{
Modes = new Dictionary<string, List<string>>
{
{ "search", new List<string> { "q" } }
}
})
.Build();
Mocker.SetConstant<CardigannDefinition>(_definition);
}
[TestCase("{{ range .Categories}}&categories[]={{.}}{{end}}", "&categories[]=tv&categories[]=movies")]
[TestCase("{{ range $i, $e := .Categories}}&categories[{{$i}}]={{.}}{{end}}", "&categories[0]=tv&categories[1]=movies")]
[TestCase("{{ range $index, $element := .Categories}}&categories[{{$index}}]={{.}}+postIndex[{{$index}}]{{end}}", "&categories[0]=tv+postIndex[0]&categories[1]=movies+postIndex[1]")]
public void should_handle_range_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ re_replace .Query.Keywords \"[^a-zA-Z0-9]+\" \"%\" }}", "abc%def")]
public void should_handle_re_replace_statements(string template, string expected)
{
_variables[".Query.Keywords"] = string.Join(" ", new List<string> { "abc", "def" });
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ join .Categories \", \" }}", "tv, movies")]
public void should_handle_join_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ .Today.Year }}", "2022")]
public void should_handle_variables_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
[TestCase("{{if .True }}0{{else}}1{{end}}", "0")]
public void should_handle_if_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
}
}

View File

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

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
{
[TestFixture]
public class OrpheusFixture : CoreTest<Orpheus>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Orpheus",
Settings = new OrpheusSettings() { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_GazelleGames()
{
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/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(65);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-08-08 2:07:39"));
torrentInfo.Size.Should().Be(68296866);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(0);
torrentInfo.Seeders.Should().Be(0);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
torrents.Should().HaveCount(293);
torrents.First().Should().BeOfType<PassThePopcornInfo>();
torrents.First().Should().BeOfType<TorrentInfo>();
var first = torrents.First() as TorrentInfo;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.SecurityTests
{
[TestFixture]
public class ProtectionServiceFixture : CoreTest<ProtectionService>
{
private string _protectionKey;
[SetUp]
public void Setup()
{
_protectionKey = Guid.NewGuid().ToString().Replace("-", "");
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadProtectionKey)
.Returns(_protectionKey);
}
[Test]
public void should_encrypt_and_decrypt_string()
{
const string plainText = "https://prowlarr.com";
var encrypted = Subject.Protect(plainText);
var decrypted = Subject.UnProtect(encrypted);
decrypted.Should().Be(plainText);
}
}
}

View File

@@ -68,6 +68,10 @@ namespace NzbDrone.Core.Test.UpdateTests
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
}
@@ -149,7 +153,7 @@ namespace NzbDrone.Core.Test.UpdateTests
}
[Test]
public void should_start_update_client()
public void should_start_update_client_if_updater_exists()
{
Subject.Execute(new ApplicationUpdateCommand());
@@ -157,6 +161,21 @@ namespace NzbDrone.Core.Test.UpdateTests
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Once());
}
[Test]
public void should_return_with_warning_if_updater_doesnt_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.IsAny<string>(), null, null, null), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_without_error_or_warnings_when_no_updates_are_available()
{

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Lidarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -40,8 +43,8 @@ namespace NzbDrone.Core.Applications.Lidarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -55,7 +58,7 @@ namespace NzbDrone.Core.Applications.Lidarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Radarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -51,7 +54,7 @@ namespace NzbDrone.Core.Applications.Radarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Readarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -40,8 +43,8 @@ namespace NzbDrone.Core.Applications.Readarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -55,7 +58,7 @@ namespace NzbDrone.Core.Applications.Readarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,11 +28,14 @@ namespace NzbDrone.Core.Applications.Sonarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var animeCats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "animeCategories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -56,7 +59,7 @@ namespace NzbDrone.Core.Applications.Sonarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Whisparr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -51,7 +54,7 @@ namespace NzbDrone.Core.Applications.Whisparr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -5,12 +5,14 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -67,6 +69,7 @@ namespace NzbDrone.Core.Configuration
private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -76,12 +79,14 @@ namespace NzbDrone.Core.Configuration
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider)
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@@ -195,13 +200,13 @@ namespace NzbDrone.Core.Configuration
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public string Theme => GetValue("Theme", "light", persist: false);
public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string Theme => GetValue("Theme", "auto", persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);

View File

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

View File

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

View File

@@ -167,14 +167,12 @@ namespace NzbDrone.Core.Datastore
}
}
if (_database.DatabaseType == DatabaseType.SQLite)
{
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
else
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
}
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using Newtonsoft.Json.Linq;
@@ -21,6 +22,8 @@ namespace NzbDrone.Core.Datastore.Migration
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'";
var updatedIndexers = new List<Indexer008>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
@@ -45,19 +48,26 @@ namespace NzbDrone.Core.Datastore.Migration
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
using (var updateCmd = conn.CreateCommand())
updatedIndexers.Add(new Indexer008
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?";
updateCmd.AddParameter(settings);
updateCmd.AddParameter("RedactedSettings");
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
Id = id,
Settings = settings,
ConfigContract = "RedactedSettings",
Enable = false
});
}
}
}
}
}
public class Indexer008
{
public int Id { get; set; }
public string Settings { get; set; }
public string ConfigContract { get; set; }
public bool Enable { get; set; }
}
}
}

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