Compare commits

...

223 Commits

Author SHA1 Message Date
Bakerboy448 5e15054329 Avistaz response improvements 2023-01-05 15:40:22 -06:00
bakerboy448 3dfbfd07dd improve authentication required wording 2023-01-04 22:53:38 -06:00
Qstick 842df6913c New: Improve CF Detection
Co-Authored-By: Diego Heras <ngosang@hotmail.es>
2023-01-04 22:27:03 -06:00
Qstick 599eeb4c61 Bump Version to 1.1.0 2023-01-03 18:50:42 -06:00
Bogdan da371dd921 Fixed: (Avistaz) Use type password for PID 2023-01-03 18:49:17 -06:00
Bogdan fc25ba7ac0 Fixed: (Filelist) Use UTC in tests 2023-01-03 18:05:54 -06:00
Qstick 6e1bef13e2 Fixed: Correctly calculate UI age for some indexers 2023-01-02 23:28:36 -06:00
Qstick 72ee413411 Fixed: (BeyondHD) Assume Universal Time for publish dates 2023-01-02 23:23:56 -06:00
Qstick e87b45b47e Fixed: (Filelist) Assume UTC+2 for API Dates 2023-01-02 23:23:17 -06:00
Qstick cc841fe3d1 Remove Preview from Page Title 2023-01-02 18:06:34 -06:00
Qstick 264ffdcc26 Fixup Provider Tests 2023-01-02 17:41:41 -06:00
Qstick 5cc044aa8f Rarbg Rate Limit Tweaks, Additional back-off level 2023-01-02 17:16:46 -06:00
Bogdan de2fd92b6f Fixed: (Avistaz) Workaround for fetching "retry-after" header not present when using "Accept: application/json" 2023-01-01 15:31:26 -06:00
Qstick eff09c1f72 Treat Master as a valid branch 2022-12-31 19:32:06 -06:00
Qstick 9db888c9a3 Bump Version to 1.0.1 2022-12-31 18:49:38 -06:00
Qstick bf78396164 Rewrite test to avoid 6 hours of failures and needing a change every year 2022-12-31 18:22:07 -06:00
Qstick 0e7eaa9221 Simplify logic in HandleRequest 2022-12-31 17:57:54 -06:00
Colin Gagnaire 5b82decc31 New: Add support for native Freebox Download Client 2022-12-31 17:34:17 -06:00
Mark McDowall 38ab533272 Fixed: Only log /proc/mounts exception once per process 2022-12-31 17:11:05 -06:00
Qstick 4914fcd5df Fixed: (UI) Category is None in history if only search by sub category 2022-12-31 17:03:27 -06:00
Qstick 858415b037 Fixed: (Cardigann) Query string gets first letter removed in request handling 2022-12-31 16:37:15 -06:00
Bogdan 43f4899324 New: (Indexer) Torrent Libble with 2FA and pagination 2022-12-31 16:22:06 -06:00
Bogdan c60a94adfb Fixed: (RetroFlix) Set 5 days as MST, return 100 results and remove "[REQUESTED]" from title 2022-12-31 16:19:47 -06:00
Qstick f386ddb806 Fixed: Sorting on mobile search UI 2022-12-31 16:17:54 -06:00
Qstick 4175c2577e Fixed: Link to release page from mobile search 2022-12-31 15:56:29 -06:00
Qstick 6ce9e5ceb9 Fixed: Release Grab not working on Mobile search 2022-12-30 18:28:55 -06:00
Bogdan c15643be39 Fixed: (Cardigann) Allow use of template variables in fields selector 2022-12-30 17:58:24 -06:00
Bogdan a58380031d Fixed: (Indexer) Added TvSearchParam.ImdbId to SpeedApp 2022-12-28 23:56:08 -06:00
Bogdan 73af5c9a72 Fixed: (Indexer) Changed FL to use internal flag 2022-12-28 23:54:48 -06:00
Bogdan d556545e7f Fixed: Changed torznab parser to use additional attributes 2022-12-28 23:54:01 -06:00
Bogdan affde5d7b7 Fixed: (Orpheus) Changed to use filters for categories, label and year 2022-12-27 22:16:49 -06:00
Bakerboy448 518c85dee2 Fixed: (Rarbg) Improve RateLimit Handling
Fixed: (Rarbg) Increase delay to 4s to reduce Rate Limiting

Fixes #1169
2022-12-26 18:26:43 -06:00
Bakerboy448 ba3a240707 Add TooManyRequestsException with var retryWait 2022-12-26 18:26:43 -06:00
Qstick 587a73f3d6 Fixed: (Newznab) Parsing of Ids from non-standard feeds
Fixes #1261
2022-12-26 18:17:50 -06:00
Weblate ae8f017ca8 Translated using Weblate (Ukrainian)
Currently translated at 63.2% (298 of 471 strings)

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

Currently translated at 98.5% (464 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Bengali)

Currently translated at 0.8% (4 of 468 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (468 of 468 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>
Co-authored-by: andrey4korop <andrey999@i.ua>
Co-authored-by: lhquark <lhquark@gmail.com>
Co-authored-by: saambd <me@salimrahman.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bn/
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/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-12-26 17:52:43 -06:00
Bogdan d9098b612e Fixed: (Nebulance) Changed MinimumSeedTime according to their H&R rules 2022-12-26 17:48:29 -06:00
Qstick 29e7cc06a1 Bump MonoTorrent to 2.0.7
Co-Authored-By: Winter <78392041+winterqt@users.noreply.github.com>
2022-12-24 15:16:00 -06:00
Qstick 387fb0bd15 Revert release analytics 2022-12-22 18:52:38 -06:00
Qstick 2d33560d89 Theme tweaks 2022-12-22 15:29:26 -06:00
Qstick 94a797fc1e New: (Nebulace) TVMaze Search Support
Fixes #1252
2022-12-22 14:32:03 -06:00
Qstick 2e851b0588 New: Mobile friendly manual search
Fixes #490
2022-12-22 13:37:09 -06:00
Qstick 7303cdf555 Fixed: Incorrect logic for newznab category parsing 2022-12-22 10:05:52 -06:00
Qstick 6636cbc4ae Fixed: (AnimeTosho) Mapping of Subcat as Parent 2022-12-22 08:47:45 -06:00
Weblate a5a4f62f25 Translated using Weblate (Bengali)
Currently translated at 0.8% (4 of 468 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: saambd <me@salimrahman.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bn/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translation: Servarr/Prowlarr
2022-12-21 15:53:44 -06:00
bakerboy448 05a7465a07 Fixed: (MTV) Torznab Api Path
#1235
2022-12-21 15:01:51 -06:00
Qstick c35f1212fb New: (Indexer) Torrent Bytes 2022-12-20 21:57:17 -06:00
Qstick ad95d73e9d Fixed: Category parsing for some not-so-great Torznab feeds
MoreThanTv
2022-12-20 20:50:30 -06:00
Qstick 30f53c20ed New: Convert MoreThanTV to API
Fixes #1235
2022-12-20 17:59:50 -06:00
Servarr 0199a37a0c Automated API Docs update 2022-12-20 16:37:56 -06:00
Qstick e9764820c0 Fixed: (SecretCinema) Title not being decoded
Fixes #1238
2022-12-20 16:24:34 -06:00
Qstick d285cbb021 (SecretCinema) Avoid double HtmlDecode
#1238
2022-12-20 15:20:06 -06:00
Qstick 8afaa3386d Fixed: (PrivateHD) Remove Audio category
Fixes #1246
2022-12-20 15:12:07 -06:00
Qstick c94beb6814 Fixed: Translations for Auth Settings 2022-12-20 14:58:58 -06:00
Qstick c7eb08a0f0 New: Auth Required
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-12-20 14:23:09 -06:00
Qstick 2a2e859420 Update UI Dependencies 2022-12-20 14:23:09 -06:00
Qstick 31f0e8212e Update UI Dev Dependencies 2022-12-20 14:23:09 -06:00
Qstick 1cbb9b1724 Bump dotnet to 6.0.12 2022-12-20 14:23:09 -06:00
Qstick 45dbcc6b89 Bump version to 1.0.0 2022-12-20 14:23:09 -06:00
Qstick 3b26613394 Fixed: (Pornolab) Update Categories
Co-Authored-By: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2022-12-20 13:38:01 -06:00
Bakerboy448 6bb8c09fcf Log Skipped Application-Indexer Syncs at trace for support 2022-12-20 13:07:32 -06:00
Qstick 810b3612aa Fixed: Mapping of Year, Genre, other from search string 2022-12-20 12:34:56 -06:00
Qstick 57dcd861a9 Fixed: Validation for nested settings not running
Prevents #1243
2022-12-18 23:20:46 -06:00
Qstick dfe132cda2 Fixed: Retain direct Indexer properties not affiliated with Prowlarr
Fixes #1165
2022-12-18 21:46:14 -06:00
Qstick a635820b48 New: Sync Indexers button on index page
Fixes #92
2022-12-18 21:33:30 -06:00
Qstick d959e81efb Modify Nab tests to pass for additional parameters
Fixes #1236
2022-12-18 21:19:03 -06:00
Qstick ac89cd636f New: Separate setting for Pack Seed Time 2022-12-18 20:52:07 -06:00
Qstick 50616f5c9e Fixed: Don't mess with options we don't set on full sync 2022-12-18 20:52:07 -06:00
Qstick 3f9cb2c6ea Fixed: String compare in arr Indexer equality 2022-12-18 20:52:06 -06:00
Qstick b5aa85a548 New: (Nebulance) Convert to API 2022-12-18 18:12:18 -06:00
Qstick 0fa5127c83 Cleanup dev logging in UI 2022-12-18 12:56:03 -06:00
Weblate 4d137886bc Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 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>
Co-authored-by: benniblot <ben2004engler@gmail.com>
Co-authored-by: mhng98 <mark.groenewegen@hotmail.com>
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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-12-18 10:41:16 -06:00
Qstick 9dde041c99 New: Search by description on add indexer modal
Fixes #1000
2022-12-18 00:13:44 -06:00
Qstick a8234c9ce0 Fixed: Refresh applicable healthchecks on bulk deletes 2022-12-18 00:02:59 -06:00
Qstick 9227efdb65 New: (FileList) Freeleech Only option
Fixes #1147
2022-12-17 23:34:08 -06:00
Qstick fa923e658f Fixed: (Nyaa) Torrent Age in UI incorrect
Fixes #144
2022-12-17 23:14:56 -06:00
bakerboy448 364a5564ae update-no-results-msg 2022-12-17 21:34:06 -06:00
Qstick 9efd0b391e fixup! 2022-12-17 21:31:37 -06:00
Qstick 320161e051 New: Smarter Newznab category mapping 2022-12-17 21:31:37 -06:00
Servarr 38ba810ae8 Automated API Docs update 2022-12-17 14:18:15 -06:00
Bakerboy448 4e3f460a24 Fixed: (Avistaz Family) Correct Age Parsing
Co-authored-by: Qstick <qstick@gmail.com>
2022-12-17 14:12:23 -06:00
Qstick 0d918a0aa9 New: Define multiple mapped categories for Download Clients
Fixes #170
2022-12-17 14:11:09 -06:00
Qstick a110412665 Fixed: Stats failing of all indexer events are failures
Fixes #1231
2022-12-17 10:27:14 -06:00
Weblate 6c97f1b6ee Translated using Weblate (Italian)
Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Finnish)

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 (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Mipiaceanutella <remix-polity-0l@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Robin Flikkema <robin@robinflikkema.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: kamperzoid <nick@kamper.be>
Co-authored-by: reloxx <reloxx@interia.pl>
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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translation: Servarr/Prowlarr
2022-12-11 12:47:35 -06:00
Qstick 470779ead2 Bump version to 0.4.11 2022-12-11 12:46:58 -06:00
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
402 changed files with 67759 additions and 6563 deletions
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered. description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting. description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+132
View File
@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@prowlarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
-3
View File
@@ -27,10 +27,7 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
## Support ## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr) [![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
+88 -53
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.2' majorVersion: '1.1.0'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.301' dotnetVersion: '6.0.404'
innoVersion: '6.2.0' innoVersion: '6.2.0'
nodeVersion: '16.x' nodeVersion: '16.x'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -97,15 +97,14 @@ stages:
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled" echo "Extra platforms already enabled"
else else
echo "Enabling BSD support" echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
displayName: Enable FreeBSD Support displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-bsd - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Prowlarr Backend displayName: Build Prowlarr Backend
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -119,24 +118,28 @@ stages:
displayName: Publish Backend displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests artifact: win-x64-tests
displayName: Publish Windows Test Package displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests artifact: linux-x64-tests
displayName: Publish Linux Test Package displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests artifact: linux-musl-x64-tests
displayName: Publish Linux Musl Test Package displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests artifact: freebsd-x64-tests
displayName: Publish FreeBSD Test Package displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests artifact: osx-x64-tests
displayName: Publish MacOS Test Package displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
@@ -239,35 +242,35 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd - bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Prowlarr" -exec chmod a+x {} \; find . -name "Prowlarr" -exec chmod a+x {} \;
find . -name "Prowlarr.Update" -exec chmod a+x {} \; find . -name "Prowlarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows Core zip displayName: Create win-x64 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Windows x86 Core zip displayName: Create win-x86 zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core app displayName: Create osx-x64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS x64 Core tar displayName: Create osx-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -275,14 +278,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app displayName: Create osx-arm64 app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar displayName: Create osx-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -290,7 +293,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Core tar displayName: Create linux-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -298,7 +301,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Musl Core tar displayName: Create linux-musl-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -306,7 +309,15 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -314,7 +325,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar displayName: Create linux-musl-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -322,7 +333,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar displayName: Create linux-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -330,7 +341,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar displayName: Create linux-musl-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -405,22 +416,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'FreebsdCore' testName: 'freebsd-x64'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -439,7 +450,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -469,8 +480,12 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -481,9 +496,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -513,14 +534,14 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost' Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432' Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr' Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
@@ -585,17 +606,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
@@ -612,7 +633,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -654,7 +675,7 @@ stages:
Prowlarr__Postgres__Password: 'prowlarr' Prowlarr__Postgres__Password: 'prowlarr'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: ${{ variables.linuxImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
@@ -666,7 +687,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'LinuxCoreTests' artifactName: 'linux-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -720,14 +741,14 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'FreebsdCoreTests' artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '/$(pattern)' itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -756,11 +777,15 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'linux-musl-x64'
artifactName: LinuxMuslCoreTests artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -770,9 +795,15 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .NET'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -818,16 +849,19 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Prowlarr.*.linux-core-x64.tar.gz' pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
failBuild: true failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Prowlarr.*.osx-core-x64.tar.gz' pattern: 'Prowlarr.*.osx-core-x64.tar.gz'
failBuild: true failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Prowlarr.*.windows-core-x64.zip' pattern: 'Prowlarr.*.windows-core-x64.zip'
failBuild: true failBuild: true
@@ -845,7 +879,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(osName)CoreTests' artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -1074,4 +1108,5 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+32 -13
View File
@@ -25,15 +25,22 @@ UpdateVersionNumber()
fi fi
} }
EnableBsdSupport() EnableExtraPlatformsInSDK()
{ {
#todo enable sdk with SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -293,7 +300,8 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_BSD=NO ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -305,8 +313,12 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd) --enable-bsd|--enable-extra-platforms)
ENABLE_BSD=YES ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -350,12 +362,17 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
EnableBsdSupport EnableExtraPlatforms
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -365,9 +382,10 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -406,9 +424,10 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_BSD" = "YES" ]; if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
@@ -16,6 +16,7 @@ import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import InfoInput from './InfoInput'; import InfoInput from './InfoInput';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import NewznabCategorySelectInputConnector from './NewznabCategorySelectInputConnector';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
@@ -68,6 +69,9 @@ function getComponent(type) {
case inputTypes.PATH: case inputTypes.PATH:
return PathInputConnector; return PathInputConnector;
case inputTypes.CATEGORY_SELECT:
return NewznabCategorySelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT: case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector; return IndexerFlagsSelectInputConnector;
@@ -31,7 +31,7 @@ function createMapStateToProps() {
}); });
return { return {
value, value: value || [],
values values
}; };
} }
@@ -36,7 +36,6 @@ class TagInputInput extends Component {
<div <div
ref={forwardedRef} ref={forwardedRef}
className={className} className={className}
component="div"
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
> >
{ {
+1 -1
View File
@@ -36,5 +36,5 @@
/** Outline **/ /** Outline **/
.outline { .outline {
background-color: var(--white); background-color: var(--cardBackgroundColor);
} }
+1 -1
View File
@@ -108,5 +108,5 @@
/** Outline **/ /** Outline **/
.outline { .outline {
background-color: var(--white); background-color: var(--cardBackgroundColor);
} }
@@ -5,7 +5,7 @@
text-align: center; text-align: center;
&:hover { &:hover {
color: var(--toobarButtonHoverColor); color: #515253;
} }
} }
+7
View File
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext'; import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector'; import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape'; import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader'; import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar'; import PageSidebar from './Sidebar/PageSidebar';
@@ -75,6 +76,7 @@ class Page extends Component {
isSmallScreen, isSmallScreen,
isSidebarVisible, isSidebarVisible,
enableColorImpairedMode, enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle, onSidebarToggle,
onSidebarVisibleChange onSidebarVisibleChange
} = this.props; } = this.props;
@@ -109,6 +111,10 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen} isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose} onModalClose={this.onConnectionLostModalClose}
/> />
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div> </div>
</ColorImpairedContext.Provider> </ColorImpairedContext.Provider>
); );
@@ -124,6 +130,7 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired, isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired, isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired, enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired, onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
+6 -15
View File
@@ -7,10 +7,11 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions'; import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage'; import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage'; import LoadingPage from './LoadingPage';
import Page from './Page'; import Page from './Page';
@@ -48,7 +49,6 @@ const selectIsPopulated = createSelector(
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
(state) => state.settings.general.isPopulated, (state) => state.settings.general.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.appProfiles.isPopulated, (state) => state.settings.appProfiles.isPopulated,
(state) => state.indexers.isPopulated, (state) => state.indexers.isPopulated,
(state) => state.indexerStatus.isPopulated, (state) => state.indexerStatus.isPopulated,
@@ -59,7 +59,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
generalSettingsIsPopulated, generalSettingsIsPopulated,
languagesIsPopulated,
appProfilesIsPopulated, appProfilesIsPopulated,
indexersIsPopulated, indexersIsPopulated,
indexerStatusIsPopulated, indexerStatusIsPopulated,
@@ -71,7 +70,6 @@ const selectIsPopulated = createSelector(
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
generalSettingsIsPopulated && generalSettingsIsPopulated &&
languagesIsPopulated &&
appProfilesIsPopulated && appProfilesIsPopulated &&
indexersIsPopulated && indexersIsPopulated &&
indexerStatusIsPopulated && indexerStatusIsPopulated &&
@@ -86,7 +84,6 @@ const selectErrors = createSelector(
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
(state) => state.settings.general.error, (state) => state.settings.general.error,
(state) => state.settings.languages.error,
(state) => state.settings.appProfiles.error, (state) => state.settings.appProfiles.error,
(state) => state.indexers.error, (state) => state.indexers.error,
(state) => state.indexerStatus.error, (state) => state.indexerStatus.error,
@@ -97,7 +94,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -109,7 +105,6 @@ const selectErrors = createSelector(
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
generalSettingsError || generalSettingsError ||
languagesError ||
appProfilesError || appProfilesError ||
indexersError || indexersError ||
indexerStatusError || indexerStatusError ||
@@ -123,7 +118,6 @@ const selectErrors = createSelector(
tagsError, tagsError,
uiSettingsError, uiSettingsError,
generalSettingsError, generalSettingsError,
languagesError,
appProfilesError, appProfilesError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
@@ -140,18 +134,21 @@ function createMapStateToProps() {
selectErrors, selectErrors,
selectAppProps, selectAppProps,
createDimensionsSelector(), createDimensionsSelector(),
createSystemStatusSelector(),
( (
enableColorImpairedMode, enableColorImpairedMode,
isPopulated, isPopulated,
errors, errors,
app, app,
dimensions dimensions,
systemStatus
) => { ) => {
return { return {
...app, ...app,
...errors, ...errors,
isPopulated, isPopulated,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode enableColorImpairedMode
}; };
} }
@@ -166,9 +163,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchTags() { dispatchFetchTags() {
dispatch(fetchTags()); dispatch(fetchTags());
}, },
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexers() { dispatchFetchIndexers() {
dispatch(fetchIndexers()); dispatch(fetchIndexers());
}, },
@@ -216,7 +210,6 @@ class PageConnector extends Component {
if (!this.props.isPopulated) { if (!this.props.isPopulated) {
this.props.dispatchFetchCustomFilters(); this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchAppProfiles(); this.props.dispatchFetchAppProfiles();
this.props.dispatchFetchIndexers(); this.props.dispatchFetchIndexers();
this.props.dispatchFetchIndexerStatus(); this.props.dispatchFetchIndexerStatus();
@@ -242,7 +235,6 @@ class PageConnector extends Component {
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchTags, dispatchFetchTags,
dispatchFetchLanguages,
dispatchFetchAppProfiles, dispatchFetchAppProfiles,
dispatchFetchIndexers, dispatchFetchIndexers,
dispatchFetchIndexerStatus, dispatchFetchIndexerStatus,
@@ -283,7 +275,6 @@ PageConnector.propTypes = {
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired, dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchAppProfiles: PropTypes.func.isRequired, dispatchFetchAppProfiles: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired, dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchFetchIndexerStatus: PropTypes.func.isRequired, dispatchFetchIndexerStatus: PropTypes.func.isRequired,
@@ -20,9 +20,9 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.MOVIE_CONTINUING, iconName: icons.MOVIE_CONTINUING,
title: 'Indexers', title: translate('Indexers'),
to: '/', to: '/',
alias: '/movies', alias: '/indexers',
children: [ children: [
{ {
title: translate('Stats'), title: translate('Stats'),
@@ -33,13 +33,13 @@ const links = [
{ {
iconName: icons.SEARCH, iconName: icons.SEARCH,
title: 'Search', title: translate('Search'),
to: '/search' to: '/search'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: 'History', title: translate('History'),
to: '/history' to: '/history'
}, },
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModal(props) {
const {
isOpen
} = props;
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AuthenticationRequiredModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AuthenticationRequiredModal.propTypes = {
isOpen: PropTypes.bool.isRequired
};
export default AuthenticationRequiredModal;
@@ -0,0 +1,5 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
@@ -0,0 +1,165 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('AuthenticationRequired')}
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{authenticationRequiredWarning}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup> :
null
}
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default AuthenticationRequiredModalContent;
@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
const SECTION = 'general';
function createMapStateToProps() {
return createSelector(
createSettingsSectionSelector(SECTION),
(sectionSettings) => {
return {
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchClearPendingChanges: clearPendingChanges,
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
dispatchSaveGeneralSettings: saveGeneralSettings,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchFetchStatus: fetchStatus
};
class AuthenticationRequiredModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchGeneralSettings();
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetGeneralSettingsValue({ name, value });
};
onSavePress = () => {
this.props.dispatchSaveGeneralSettings();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchFetchGeneralSettings,
dispatchSetGeneralSettingsValue,
dispatchSaveGeneralSettings,
...otherProps
} = this.props;
return (
<AuthenticationRequiredModalContent
{...otherProps}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
AuthenticationRequiredModalContentConnector.propTypes = {
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
+2
View File
@@ -8,6 +8,7 @@ export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const INFO = 'info'; export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@@ -32,6 +33,7 @@ export const all = [
KEY_VALUE_LIST, KEY_VALUE_LIST,
INFO, INFO,
MOVIE_MONITORED_SELECT, MOVIE_MONITORED_SELECT,
CATEGORY_SELECT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
+45
View File
@@ -226,6 +226,42 @@ class HistoryRow extends Component {
null null
} }
{
data.label ?
<HistoryRowParameter
title='Label'
value={data.label}
/> :
null
}
{
data.track ?
<HistoryRowParameter
title='Track'
value={data.track}
/> :
null
}
{
data.year ?
<HistoryRowParameter
title='Year'
value={data.year}
/> :
null
}
{
data.genre ?
<HistoryRowParameter
title='Genre'
value={data.genre}
/> :
null
}
{ {
data.author ? data.author ?
<HistoryRowParameter <HistoryRowParameter
@@ -243,6 +279,15 @@ class HistoryRow extends Component {
/> : /> :
null null
} }
{
data.publisher ?
<HistoryRowParameter
title='Publisher'
value={data.publisher}
/> :
null
}
</TableRowCell> </TableRowCell>
); );
} }
@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
const filteredIndexers = indexers.filter((indexer) => { const filteredIndexers = indexers.filter((indexer) => {
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state; const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) { if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
return false; return false;
} }
+13 -1
View File
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
onKeyUp = (event) => { onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order; const jumpBarItems = this.state.jumpBarItems.order;
if (event.path.length === 4) { if (event.composedPath && event.composedPath().length === 4) {
if (event.keyCode === keyCodes.HOME && event.ctrlKey) { if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] }); this.setState({ jumpToCharacter: jumpBarItems[0] });
} }
@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
saveError, saveError,
isDeleting, isDeleting,
isTestingAll, isTestingAll,
isSyncingIndexers,
deleteError, deleteError,
onScroll, onScroll,
onSortSelect, onSortSelect,
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
onPress={this.onAddIndexerPress} onPress={this.onAddIndexerPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={this.props.onAppIndexerSyncPress}
/>
<PageToolbarButton <PageToolbarButton
label={translate('TestAllIndexers')} label={translate('TestAllIndexers')}
iconName={icons.TEST} iconName={icons.TEST}
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
saveError: PropTypes.object, saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
isTestingAll: PropTypes.bool.isRequired, isTestingAll: PropTypes.bool.isRequired,
isSyncingIndexers: PropTypes.bool.isRequired,
deleteError: PropTypes.object, deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onTestAllPress: PropTypes.func.isRequired, onTestAllPress: PropTypes.func.isRequired,
onAppIndexerSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired, onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired onSaveSelected: PropTypes.func.isRequired
}; };
@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions'; import { testAllIndexers } from 'Store/Actions/indexerActions';
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions'; import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector'; import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import IndexerIndex from './IndexerIndex'; import IndexerIndex from './IndexerIndex';
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex'), createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
createDimensionsSelector(), createDimensionsSelector(),
( (
indexers, indexers,
isSyncingIndexers,
dimensionsState dimensionsState
) => { ) => {
return { return {
...indexers, ...indexers,
isSyncingIndexers,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
onTestAllPress() { onTestAllPress() {
dispatch(testAllIndexers()); dispatch(testAllIndexers());
},
onAppIndexerSyncPress() {
dispatch(executeCommand({
name: commandNames.APP_INDEXER_SYNC
}));
} }
}; };
} }
@@ -35,21 +35,21 @@ class IndexerIndexFooter extends PureComponent {
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.enabled} /> <div className={styles.enabled} />
<div> <div>
Enabled {translate('Enabled')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.redirected} /> <div className={styles.redirected} />
<div> <div>
Enabled, Redirected {translate('EnabledRedirected')}
</div> </div>
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.disabled} /> <div className={styles.disabled} />
<div> <div>
Disabled {translate('Disabled')}
</div> </div>
</div> </div>
@@ -60,7 +60,7 @@ class IndexerIndexFooter extends PureComponent {
)} )}
/> />
<div> <div>
Error {translate('Error')}
</div> </div>
</div> </div>
</div> </div>
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
let filteredList = categories.filter((item) => item.id < 100000); let filteredList = categories.filter((item) => item.id < 100000);
if (categoryFilter.length > 0) { if (categoryFilter.length > 0) {
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id)); filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
} }
const nameList = filteredList.map((item) => item.name).sort(); const nameList = filteredList.map((item) => item.name).sort();
@@ -0,0 +1,49 @@
$hoverScale: 1.05;
.content {
display: flex;
flex-grow: 0;
margin-left: 5px;
}
.container {
border-radius: 4px;
background-color: var(--cardBackgroundColor);
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.titleRow {
display: flex;
justify-content: space-between;
flex: 0 0 auto;
margin-bottom: 10px;
height: 38px;
}
.indexerRow {
color: var(--disabledColor);
}
.infoRow {
margin-bottom: 5px;
}
.title {
overflow: hidden;
width: 85%;
font-weight: 500;
font-size: 14px;
overflow-wrap: break-word;
}
.actions {
position: absolute;
right: 0;
white-space: nowrap;
}
@@ -0,0 +1,202 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons, kinds } from 'Helpers/Props';
import CategoryLabel from 'Search/Table/CategoryLabel';
import Peers from 'Search/Table/Peers';
import ProtocolLabel from 'Search/Table/ProtocolLabel';
import dimensions from 'Styles/Variables/dimensions';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './SearchIndexOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
class SearchIndexOverview extends Component {
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
};
//
// Render
render() {
const {
title,
infoUrl,
protocol,
downloadUrl,
categories,
seeders,
leechers,
size,
age,
ageHours,
ageMinutes,
indexer,
rowHeight,
isSmallScreen,
isGrabbed,
isGrabbing,
grabError
} = this.props;
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.info} style={{ height: contentHeight }}>
<div className={styles.titleRow}>
<div className={styles.title}>
<Link
to={infoUrl}
title={title}
>
<TextTruncate
line={2}
text={title}
/>
</Link>
</div>
<div className={styles.actions}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl}
/>
</div>
</div>
<div className={styles.indexerRow}>
{indexer}
</div>
<div className={styles.infoRow}>
<ProtocolLabel
protocol={protocol}
/>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
<Label>
{formatBytes(size)}
</Label>
<Label>
{formatAge(age, ageHours, ageMinutes)}
</Label>
<CategoryLabel
categories={categories}
/>
</div>
</div>
</div>
</div>
);
}
}
SearchIndexOverview.propTypes = {
guid: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
files: PropTypes.number,
grabs: PropTypes.number,
seeders: PropTypes.number,
leechers: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
rowHeight: PropTypes.number.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onGrabPress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string
};
SearchIndexOverview.defaultProps = {
isGrabbing: false,
isGrabbed: false
};
export default SearchIndexOverview;
@@ -0,0 +1,11 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: var(--tableRowHoverBackgroundColor);
}
}
}
@@ -0,0 +1,209 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import SearchIndexOverview from './SearchIndexOverview';
import styles from './SearchIndexOverviews.css';
class SearchIndexOverviews extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnCount: 1,
rowHeight: 100,
scrollRestored: false
};
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
sortKey,
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
const {
width,
rowHeight,
scrollRestored
} = this.state;
if (prevProps.sortKey !== sortKey) {
this.calculateGrid(this.state.width, isSmallScreen);
}
if (
this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
)
) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
}
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const rowHeight = 100;
this.setState({
width,
rowHeight
});
};
cellRenderer = ({ key, rowIndex, style }) => {
const {
items,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen,
onGrabPress
} = this.props;
const {
rowHeight
} = this.state;
const release = items[rowIndex];
return (
<div
className={styles.container}
key={key}
style={style}
>
<SearchIndexItemConnector
key={release.guid}
component={SearchIndexOverview}
rowHeight={rowHeight}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
guid={release.guid}
onGrabPress={onGrabPress}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
rowHeight
} = this.state;
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}
SearchIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default SearchIndexOverviews;
@@ -0,0 +1,32 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabRelease } from 'Store/Actions/releaseActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexOverviews from './SearchIndexOverviews';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
createDimensionsSelector(),
(uiSettings, dimensions) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onGrabPress(payload) {
dispatch(grabRelease(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
+8 -33
View File
@@ -11,8 +11,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props'; import { align, icons, sortDirections } from 'Helpers/Props';
import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer'; import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu'; import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu'; import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
import NoSearchResults from './NoSearchResults'; import NoSearchResults from './NoSearchResults';
import SearchFooterConnector from './SearchFooterConnector'; import SearchFooterConnector from './SearchFooterConnector';
import SearchIndexTableConnector from './Table/SearchIndexTableConnector'; import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
import styles from './SearchIndex.css'; import styles from './SearchIndex.css';
function getViewComponent() { function getViewComponent(isSmallScreen) {
if (isSmallScreen) {
return SearchIndexOverviewsConnector;
}
return SearchIndexTableConnector; return SearchIndexTableConnector;
} }
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
scroller: null, scroller: null,
jumpBarItems: { order: [] }, jumpBarItems: { order: [] },
jumpToCharacter: null, jumpToCharacter: null,
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false,
searchType: null, searchType: null,
lastToggled: null, lastToggled: null,
allSelected: false, allSelected: false,
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
// //
// Listeners // Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onJumpBarItemPress = (jumpToCharacter) => { onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter }); this.setState({ jumpToCharacter });
}; };
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
onScroll, onScroll,
onSortSelect, onSortSelect,
onFilterSelect, onFilterSelect,
isSmallScreen,
hasIndexers, hasIndexers,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
const { const {
scroller, scroller,
jumpBarItems, jumpBarItems,
isAddIndexerModalOpen,
isEditIndexerModalOpen,
jumpToCharacter, jumpToCharacter,
selectedState, selectedState,
allSelected, allSelected,
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
const selectedIndexerIds = this.getSelectedIds(); const selectedIndexerIds = this.getSelectedIds();
const ViewComponent = getViewComponent(); const ViewComponent = getViewComponent(isSmallScreen);
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems; const hasNoIndexer = !totalItems;
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
onSearchPress={this.onSearchPress} onSearchPress={this.onSearchPress}
onBulkGrabPress={this.onBulkGrabPress} onBulkGrabPress={this.onBulkGrabPress}
/> />
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageContent> </PageContent>
); );
} }
@@ -14,7 +14,7 @@
.category { .category {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 110px; flex: 0 0 130px;
} }
.age, .age,
@@ -18,20 +18,20 @@ function createMapStateToProps() {
return createSelector( return createSelector(
createReleaseSelector(), createReleaseSelector(),
( (
movie release
) => { ) => {
// If a movie is deleted this selector may fire before the parent // If a release is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens // selecors, which will result in an undefined release, if that happens
// we want to return early here and again in the render function to avoid // we want to return early here and again in the render function to avoid
// trying to show a movie that has no information available. // trying to show a release that has no information available.
if (!movie) { if (!release) {
return {}; return {};
} }
return { return {
...movie ...release
}; };
} }
); );
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
dispatchExecuteCommand: executeCommand dispatchExecuteCommand: executeCommand
}; };
class MovieIndexItemConnector extends Component { class SearchIndexItemConnector extends Component {
// //
// Render // Render
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
} }
} }
MovieIndexItemConnector.propTypes = { SearchIndexItemConnector.propTypes = {
guid: PropTypes.string, guid: PropTypes.string,
component: PropTypes.elementType.isRequired component: PropTypes.elementType.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector); export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
+1 -1
View File
@@ -21,7 +21,7 @@
.category { .category {
composes: cell; composes: cell;
flex: 0 0 110px; flex: 0 0 130px;
} }
.age, .age,
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AddCategoryModalContentConnector from './AddCategoryModalContentConnector';
function AddCategoryModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddCategoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddCategoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddCategoryModal;
@@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import AddCategoryModal from './AddCategoryModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.downloadClientCategories';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
}
};
}
class AddCategoryModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
...otherProps
} = this.props;
return (
<AddCategoryModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
AddCategoryModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(AddCategoryModalConnector);
@@ -0,0 +1,5 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
@@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AddCategoryModalContent.css';
function AddCategoryModalContent(props) {
const {
advancedSettings,
item,
onInputChange,
onFieldChange,
onCancelPress,
onSavePress,
onDeleteSpecificationPress,
...otherProps
} = props;
const {
id,
clientCategory,
categories
} = item;
return (
<ModalContent onModalClose={onCancelPress}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Category`}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>
{translate('DownloadClientCategory')}
</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="clientCategory"
{...clientCategory}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MappedCategories')}
</FormLabel>
<FormInputGroup
type={inputTypes.CATEGORY_SELECT}
name="categories"
{...categories}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteSpecificationPress}
>
{translate('Delete')}
</Button>
}
<Button
onPress={onCancelPress}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={false}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
AddCategoryModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onCancelPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onDeleteSpecificationPress: PropTypes.func
};
export default AddCategoryModalContent;
@@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearDownloadClientCategoryPending, saveDownloadClientCategory, setDownloadClientCategoryFieldValue, setDownloadClientCategoryValue } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import AddCategoryModalContent from './AddCategoryModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('downloadClientCategories'),
(advancedSettings, specification) => {
return {
advancedSettings,
...specification
};
}
);
}
const mapDispatchToProps = {
setDownloadClientCategoryValue,
setDownloadClientCategoryFieldValue,
saveDownloadClientCategory,
clearDownloadClientCategoryPending
};
class AddCategoryModalContentConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setDownloadClientCategoryValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setDownloadClientCategoryFieldValue({ name, value });
};
onCancelPress = () => {
this.props.clearDownloadClientCategoryPending();
this.props.onModalClose();
};
onSavePress = () => {
this.props.saveDownloadClientCategory({ id: this.props.id });
this.props.onModalClose();
};
//
// Render
render() {
return (
<AddCategoryModalContent
{...this.props}
onCancelPress={this.onCancelPress}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
AddCategoryModalContentConnector.propTypes = {
id: PropTypes.number,
item: PropTypes.object.isRequired,
setDownloadClientCategoryValue: PropTypes.func.isRequired,
setDownloadClientCategoryFieldValue: PropTypes.func.isRequired,
clearDownloadClientCategoryPending: PropTypes.func.isRequired,
saveDownloadClientCategory: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddCategoryModalContentConnector);
@@ -0,0 +1,32 @@
.customFormat {
composes: card from '~Components/Card.css';
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
margin-bottom: 5px;
font-weight: 300;
font-size: 20px;
}
.labels {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}
.tooltipLabel {
composes: label from '~Components/Label.css';
margin: 0;
border: none;
}
@@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddCategoryModalConnector from './AddCategoryModalConnector';
import styles from './Category.css';
class Category extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditSpecificationModalOpen: false,
isDeleteSpecificationModalOpen: false
};
}
//
// Listeners
onEditSpecificationPress = () => {
this.setState({ isEditSpecificationModalOpen: true });
};
onEditSpecificationModalClose = () => {
this.setState({ isEditSpecificationModalOpen: false });
};
onDeleteSpecificationPress = () => {
this.setState({
isEditSpecificationModalOpen: false,
isDeleteSpecificationModalOpen: true
});
};
onDeleteSpecificationModalClose = () => {
this.setState({ isDeleteSpecificationModalOpen: false });
};
onConfirmDeleteSpecification = () => {
this.props.onConfirmDeleteSpecification(this.props.id);
};
//
// Lifecycle
render() {
const {
id,
clientCategory,
categories
} = this.props;
return (
<Card
className={styles.customFormat}
overlayContent={true}
onPress={this.onEditSpecificationPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{clientCategory}
</div>
</div>
<Label kind={kinds.PRIMARY}>
{`${categories.length} ${categories.length > 1 ? translate('Categories') : translate('Category')}`}
</Label>
<AddCategoryModalConnector
id={id}
isOpen={this.state.isEditSpecificationModalOpen}
onModalClose={this.onEditSpecificationModalClose}
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteClientCategory')}
message={
<div>
<div>
{translate('AreYouSureYouWantToDeleteCategory', [name])}
</div>
</div>
}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose}
/>
</Card>
);
}
}
Category.propTypes = {
id: PropTypes.number.isRequired,
categories: PropTypes.arrayOf(PropTypes.number).isRequired,
clientCategory: PropTypes.string.isRequired,
onConfirmDeleteSpecification: PropTypes.func.isRequired
};
export default Category;
@@ -1,11 +1,14 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -13,12 +16,33 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AddCategoryModalConnector from './Categories/AddCategoryModalConnector';
import Category from './Categories/Category';
import styles from './EditDownloadClientModalContent.css'; import styles from './EditDownloadClientModalContent.css';
class EditDownloadClientModalContent extends Component { class EditDownloadClientModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddCategoryModalOpen: false
};
}
onAddCategoryPress = () => {
this.setState({ isAddCategoryModalOpen: true });
};
onAddCategoryModalClose = () => {
this.setState({ isAddCategoryModalOpen: false });
};
// //
// Render // Render
@@ -27,6 +51,7 @@ class EditDownloadClientModalContent extends Component {
advancedSettings, advancedSettings,
isFetching, isFetching,
error, error,
categories,
isSaving, isSaving,
isTesting, isTesting,
saveError, saveError,
@@ -37,15 +62,21 @@ class EditDownloadClientModalContent extends Component {
onSavePress, onSavePress,
onTestPress, onTestPress,
onDeleteDownloadClientPress, onDeleteDownloadClientPress,
onConfirmDeleteCategory,
...otherProps ...otherProps
} = this.props; } = this.props;
const {
isAddCategoryModalOpen
} = this.state;
const { const {
id, id,
implementationName, implementationName,
name, name,
enable, enable,
priority, priority,
supportsCategories,
fields, fields,
message message
} = item; } = item;
@@ -136,6 +167,43 @@ class EditDownloadClientModalContent extends Component {
/> />
</FormGroup> </FormGroup>
{
supportsCategories.value ?
<FieldSet legend={translate('MappedCategories')}>
<div className={styles.customFormats}>
{
categories.map((tag) => {
return (
<Category
key={tag.id}
{...tag}
onConfirmDeleteSpecification={onConfirmDeleteCategory}
/>
);
})
}
<Card
className={styles.addCategory}
onPress={this.onAddCategoryPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={25}
/>
</div>
</Card>
</div>
</FieldSet> :
null
}
<AddCategoryModalConnector
isOpen={isAddCategoryModalOpen}
onModalClose={this.onAddCategoryModalClose}
/>
</Form> </Form>
} }
</ModalBody> </ModalBody>
@@ -185,13 +253,15 @@ EditDownloadClientModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
isTesting: PropTypes.bool.isRequired, isTesting: PropTypes.bool.isRequired,
categories: PropTypes.arrayOf(PropTypes.object),
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired, onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func onDeleteDownloadClientPress: PropTypes.func,
onConfirmDeleteCategory: PropTypes.func.isRequired
}; };
export default EditDownloadClientModalContent; export default EditDownloadClientModalContent;
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions'; import { deleteDownloadClientCategory, fetchDownloadClientCategories, saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent'; import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -10,10 +10,12 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createProviderSettingsSelector('downloadClients'), createProviderSettingsSelector('downloadClients'),
(advancedSettings, downloadClient) => { (state) => state.settings.downloadClientCategories,
(advancedSettings, downloadClient, categories) => {
return { return {
advancedSettings, advancedSettings,
...downloadClient ...downloadClient,
categories: categories.items
}; };
} }
); );
@@ -23,7 +25,9 @@ const mapDispatchToProps = {
setDownloadClientValue, setDownloadClientValue,
setDownloadClientFieldValue, setDownloadClientFieldValue,
saveDownloadClient, saveDownloadClient,
testDownloadClient testDownloadClient,
fetchDownloadClientCategories,
deleteDownloadClientCategory
}; };
class EditDownloadClientModalContentConnector extends Component { class EditDownloadClientModalContentConnector extends Component {
@@ -31,6 +35,14 @@ class EditDownloadClientModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() {
const {
id,
tagsFromId
} = this.props;
this.props.fetchDownloadClientCategories({ id: tagsFromId || id });
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose(); this.props.onModalClose();
@@ -56,6 +68,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id }); this.props.testDownloadClient({ id: this.props.id });
}; };
onConfirmDeleteCategory = (id) => {
this.props.deleteDownloadClientCategory({ id });
};
// //
// Render // Render
@@ -67,6 +83,7 @@ class EditDownloadClientModalContentConnector extends Component {
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
onConfirmDeleteCategory={this.onConfirmDeleteCategory}
/> />
); );
} }
@@ -74,10 +91,13 @@ class EditDownloadClientModalContentConnector extends Component {
EditDownloadClientModalContentConnector.propTypes = { EditDownloadClientModalContentConnector.propTypes = {
id: PropTypes.number, id: PropTypes.number,
tagsFromId: PropTypes.number,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
fetchDownloadClientCategories: PropTypes.func.isRequired,
deleteDownloadClientCategory: PropTypes.func.isRequired,
setDownloadClientValue: PropTypes.func.isRequired, setDownloadClientValue: PropTypes.func.isRequired,
setDownloadClientFieldValue: PropTypes.func.isRequired, setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired,
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props'; import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const authenticationMethodOptions = [ export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
{ key: 'none', value: 'None' },
export const authenticationMethodOptions = [
{ key: 'none', value: 'None', isDisabled: true },
{ key: 'external', value: 'External', isHidden: true },
{ key: 'basic', value: 'Basic (Browser Popup)' }, { key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' } { key: 'forms', value: 'Forms (Login Page)' }
]; ];
export const authenticationRequiredOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
];
const certificateValidationOptions = [ const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' }, { key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }, { key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
const { const {
authenticationMethod, authenticationMethod,
authenticationRequired,
username, username,
password, password,
apiKey, apiKey,
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')} helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationRequiredWarning}
onChange={onInputChange} onChange={onInputChange}
{...authenticationMethod} {...authenticationMethod}
/> />
</FormGroup> </FormGroup>
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>{translate('Username')}</FormLabel> <FormLabel>{translate('Username')}</FormLabel>
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...username} {...username}
/> />
</FormGroup> </FormGroup> :
null
} }
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>{translate('Password')}</FormLabel> <FormLabel>{translate('Password')}</FormLabel>
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...password} {...password}
/> />
</FormGroup> </FormGroup> :
null
} }
<FormGroup> <FormGroup>
+1 -3
View File
@@ -62,8 +62,6 @@ class UISettings extends Component {
...otherProps ...otherProps
} = this.props; } = this.props;
const uiLanguages = languages.filter((item) => item.value !== 'Original');
const themeOptions = Object.keys(themes) const themeOptions = Object.keys(themes)
.map((theme) => ({ key: theme, value: titleCase(theme) })); .map((theme) => ({ key: theme, value: titleCase(theme) }));
@@ -172,7 +170,7 @@ class UISettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="uiLanguage" name="uiLanguage"
values={uiLanguages} values={languages}
helpText={translate('UILanguageHelpText')} helpText={translate('UILanguageHelpText')}
helpTextWarning={translate('UILanguageHelpTextWarning')} helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange} onChange={onInputChange}
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchLocalizationOptions } from 'Store/Actions/localizationActions';
import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import UISettings from './UISettings'; import UISettings from './UISettings';
@@ -11,18 +12,17 @@ const SECTION = 'ui';
function createLanguagesSelector() { function createLanguagesSelector() {
return createSelector( return createSelector(
(state) => state.settings.languages, (state) => state.localization,
(languages) => { (localization) => {
const items = languages.items; const items = localization.items;
const filterItems = ['Any', 'Unknown'];
if (!items) { if (!items) {
return []; return [];
} }
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => { const newItems = items.filter((lang) => !items.includes(lang.name)).map((item) => {
return { return {
key: item.id, key: item.value,
value: item.name value: item.name
}; };
}); });
@@ -51,6 +51,7 @@ const mapDispatchToProps = {
setUISettingsValue, setUISettingsValue,
saveUISettings, saveUISettings,
fetchUISettings, fetchUISettings,
fetchLocalizationOptions,
clearPendingChanges clearPendingChanges
}; };
@@ -61,6 +62,7 @@ class UISettingsConnector extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchUISettings(); this.props.fetchUISettings();
this.props.fetchLocalizationOptions();
} }
componentWillUnmount() { componentWillUnmount() {
@@ -96,6 +98,7 @@ UISettingsConnector.propTypes = {
setUISettingsValue: PropTypes.func.isRequired, setUISettingsValue: PropTypes.func.isRequired,
saveUISettings: PropTypes.func.isRequired, saveUISettings: PropTypes.func.isRequired,
fetchUISettings: PropTypes.func.isRequired, fetchUISettings: PropTypes.func.isRequired,
fetchLocalizationOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };
@@ -0,0 +1,169 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getNextId from 'Utilities/State/getNextId';
import getProviderState from 'Utilities/State/getProviderState';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import { removeItem, set, update, updateItem } from '../baseActions';
//
// Variables
const section = 'settings.downloadClientCategories';
//
// Actions Types
export const FETCH_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/fetchDownloadClientCategories';
export const FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/fetchDownloadClientCategorySchema';
export const SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA = 'settings/downloadClientCategories/selectDownloadClientCategorySchema';
export const SET_DOWNLOAD_CLIENT_CATEGORY_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryValue';
export const SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE = 'settings/downloadClientCategories/setDownloadClientCategoryFieldValue';
export const SAVE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/saveDownloadClientCategory';
export const DELETE_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteDownloadClientCategory';
export const DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY = 'settings/downloadClientCategories/deleteAllDownloadClientCategory';
export const CLEAR_DOWNLOAD_CLIENT_CATEGORIES = 'settings/downloadClientCategories/clearDownloadClientCategories';
export const CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING = 'settings/downloadClientCategories/clearDownloadClientCategoryPending';
//
// Action Creators
export const fetchDownloadClientCategories = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORIES);
export const fetchDownloadClientCategorySchema = createThunk(FETCH_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
export const selectDownloadClientCategorySchema = createAction(SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA);
export const saveDownloadClientCategory = createThunk(SAVE_DOWNLOAD_CLIENT_CATEGORY);
export const deleteDownloadClientCategory = createThunk(DELETE_DOWNLOAD_CLIENT_CATEGORY);
export const deleteAllDownloadClientCategory = createThunk(DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY);
export const setDownloadClientCategoryValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_VALUE, (payload) => {
return {
section,
...payload
};
});
export const setDownloadClientCategoryFieldValue = createAction(SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE, (payload) => {
return {
section,
...payload
};
});
export const clearDownloadClientCategory = createAction(CLEAR_DOWNLOAD_CLIENT_CATEGORIES);
export const clearDownloadClientCategoryPending = createThunk(CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING);
//
// Details
export default {
//
// State
defaultState: {
isPopulated: false,
error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: [],
selectedSchema: {},
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_DOWNLOAD_CLIENT_CATEGORIES]: (getState, payload, dispatch) => {
let tags = [];
if (payload.id) {
const cfState = getSectionState(getState(), 'settings.downloadClients', true);
const cf = cfState.items[cfState.itemMap[payload.id]];
tags = cf.categories.map((tag, i) => {
return {
id: i + 1,
...tag
};
});
}
dispatch(batchActions([
update({ section, data: tags }),
set({
section,
isPopulated: true
})
]));
},
[SAVE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
const {
id,
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
// we have to set id since not actually posting to server yet
if (!saveData.id) {
saveData.id = getNextId(getState().settings.downloadClientCategories.items);
}
dispatch(batchActions([
updateItem({ section, ...saveData }),
set({
section,
pendingChanges: {}
})
]));
},
[DELETE_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
const id = payload.id;
return dispatch(removeItem({ section, id }));
},
[DELETE_ALL_DOWNLOAD_CLIENT_CATEGORY]: (getState, payload, dispatch) => {
return dispatch(set({
section,
items: []
}));
},
[CLEAR_DOWNLOAD_CLIENT_CATEGORY_PENDING]: (getState, payload, dispatch) => {
return dispatch(set({
section,
pendingChanges: {}
}));
}
},
//
// Reducers
reducers: {
[SET_DOWNLOAD_CLIENT_CATEGORY_VALUE]: createSetSettingValueReducer(section),
[SET_DOWNLOAD_CLIENT_CATEGORY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SELECT_DOWNLOAD_CLIENT_CATEGORY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
return selectedSchema;
});
},
[CLEAR_DOWNLOAD_CLIENT_CATEGORIES]: createClearReducer(section, {
isPopulated: false,
error: null,
items: []
})
}
};
@@ -9,6 +9,7 @@ import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema'; import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import { set } from '../baseActions';
// //
// Variables // Variables
@@ -90,10 +91,34 @@ export default {
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'), [FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'), [FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'), [SAVE_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
// move the format tags in as a pending change
const state = getState();
const pendingChanges = state.settings.downloadClients.pendingChanges;
pendingChanges.categories = state.settings.downloadClientCategories.items;
dispatch(set({
section,
pendingChanges
}));
createSaveProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
},
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section), [CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'), [DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
[TEST_DOWNLOAD_CLIENT]: (getState, payload, dispatch) => {
const state = getState();
const pendingChanges = state.settings.downloadClients.pendingChanges;
pendingChanges.categories = state.settings.downloadClientCategories.items;
dispatch(set({
section,
pendingChanges
}));
createTestProviderHandler(section, '/downloadclient')(getState, payload, dispatch);
},
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section), [CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient') [TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
}, },
@@ -1,48 +0,0 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.languages';
//
// Actions Types
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
//
// Action Creators
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
},
//
// Reducers
reducers: {
}
};
+7 -7
View File
@@ -36,31 +36,31 @@ export const defaultState = {
}, },
{ {
name: 'indexer', name: 'indexer',
label: 'Indexer', label: translate('Indexer'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'query', name: 'query',
label: 'Query', label: translate('Query'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'parameters', name: 'parameters',
label: 'Parameters', label: translate('Parameters'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'grabTitle', name: 'grabTitle',
label: 'Grab Title', label: translate('Grab Title'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'categories', name: 'categories',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
@@ -72,13 +72,13 @@ export const defaultState = {
}, },
{ {
name: 'source', name: 'source',
label: 'Source', label: translate('Source'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'elapsedTime', name: 'elapsedTime',
label: 'Elapsed Time', label: translate('Elapsed Time'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
+2
View File
@@ -7,6 +7,7 @@ import * as indexers from './indexerActions';
import * as indexerIndex from './indexerIndexActions'; import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions'; import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions'; import * as indexerStatus from './indexerStatusActions';
import * as localization from './localizationActions';
import * as oAuth from './oAuthActions'; import * as oAuth from './oAuthActions';
import * as paths from './pathActions'; import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions'; import * as providerOptions from './providerOptionActions';
@@ -25,6 +26,7 @@ export default [
paths, paths,
providerOptions, providerOptions,
releases, releases,
localization,
indexers, indexers,
indexerIndex, indexerIndex,
indexerStats, indexerStats,
@@ -36,7 +36,7 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'select', name: 'select',
columnLabel: 'Select', columnLabel: translate('Select'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false, isModifiable: false,
@@ -51,7 +51,7 @@ export const defaultState = {
}, },
{ {
name: 'sortName', name: 'sortName',
label: 'Indexer Name', label: translate('IndexerName'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
@@ -88,7 +88,7 @@ export const defaultState = {
}, },
{ {
name: 'capabilities', name: 'capabilities',
label: 'Categories', label: translate('Categories'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
@@ -0,0 +1,39 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'localization';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_LOCALIZATION_OPTIONS = 'localization/fetchLocalizationOptions';
//
// Action Creators
export const fetchLocalizationOptions = createThunk(FETCH_LOCALIZATION_OPTIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_LOCALIZATION_OPTIONS]: createFetchHandler(section, '/localization/options')
});
//
// Reducers
export const reducers = createHandleActions({}, defaultState, section);
@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
isGrabbing: true isGrabbing: true
})); }));
console.log(payload);
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/search/bulk', url: '/search/bulk',
method: 'POST', method: 'POST',
@@ -4,19 +4,19 @@ import createHandleActions from './Creators/createHandleActions';
import applications from './Settings/applications'; import applications from './Settings/applications';
import appProfiles from './Settings/appProfiles'; import appProfiles from './Settings/appProfiles';
import development from './Settings/development'; import development from './Settings/development';
import downloadClientCategories from './Settings/downloadClientCategories';
import downloadClients from './Settings/downloadClients'; import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories'; import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies'; import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
import ui from './Settings/ui'; import ui from './Settings/ui';
export * from './Settings/downloadClientCategories';
export * from './Settings/downloadClients'; export * from './Settings/downloadClients';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/indexerCategories'; export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies'; export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications'; export * from './Settings/notifications';
export * from './Settings/applications'; export * from './Settings/applications';
export * from './Settings/appProfiles'; export * from './Settings/appProfiles';
@@ -34,11 +34,11 @@ export const section = 'settings';
export const defaultState = { export const defaultState = {
advancedSettings: false, advancedSettings: false,
downloadClientCategories: downloadClientCategories.defaultState,
downloadClients: downloadClients.defaultState, downloadClients: downloadClients.defaultState,
general: general.defaultState, general: general.defaultState,
indexerCategories: indexerCategories.defaultState, indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState, indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState, notifications: notifications.defaultState,
applications: applications.defaultState, applications: applications.defaultState,
appProfiles: appProfiles.defaultState, appProfiles: appProfiles.defaultState,
@@ -64,11 +64,11 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
...downloadClientCategories.actionHandlers,
...downloadClients.actionHandlers, ...downloadClients.actionHandlers,
...general.actionHandlers, ...general.actionHandlers,
...indexerCategories.actionHandlers, ...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers, ...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers, ...notifications.actionHandlers,
...applications.actionHandlers, ...applications.actionHandlers,
...appProfiles.actionHandlers, ...appProfiles.actionHandlers,
@@ -85,11 +85,11 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
}, },
...downloadClientCategories.reducers,
...downloadClients.reducers, ...downloadClients.reducers,
...general.reducers, ...general.reducers,
...indexerCategories.reducers, ...indexerCategories.reducers,
...indexerProxies.reducers, ...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers, ...notifications.reducers,
...applications.reducers, ...applications.reducers,
...appProfiles.reducers, ...appProfiles.reducers,
+4
View File
@@ -1,7 +1,11 @@
import * as dark from './dark'; import * as dark from './dark';
import * as light from './light'; import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
export default { export default {
auto,
light, light,
dark dark
}; };
@@ -15,27 +15,27 @@ const columns = [
}, },
{ {
name: 'commandName', name: 'commandName',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'queued', name: 'queued',
label: 'Queued', label: translate('Queued'),
isVisible: true isVisible: true
}, },
{ {
name: 'started', name: 'started',
label: 'Started', label: translate('Started'),
isVisible: true isVisible: true
}, },
{ {
name: 'ended', name: 'ended',
label: 'Ended', label: translate('Ended'),
isVisible: true isVisible: true
}, },
{ {
name: 'duration', name: 'duration',
label: 'Duration', label: translate('Duration'),
isVisible: true isVisible: true
}, },
{ {
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [ const columns = [
{ {
name: 'name', name: 'name',
label: 'Name', label: translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'interval', name: 'interval',
label: 'Interval', label: translate('Interval'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastExecution', name: 'lastExecution',
label: 'Last Execution', label: translate('LastExecution'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastDuration', name: 'lastDuration',
label: 'Last Duration', label: translate('LastDuration'),
isVisible: true isVisible: true
}, },
{ {
name: 'nextExecution', name: 'nextExecution',
label: 'Next Execution', label: translate('NextExecution'),
isVisible: true isVisible: true
}, },
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
import filesize from 'filesize'; import { filesize } from 'filesize';
function formatBytes(input) { function formatBytes(input) {
const size = Number(input); const size = Number(input);
+2 -2
View File
@@ -11,7 +11,7 @@
<!-- Windows Phone --> <!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51" /> <meta name="msapplication-navbutton-color" content="#3a3f51" />
<meta name="description" content="Prowlarr (Preview)" /> <meta name="description" content="Prowlarr" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
@@ -50,7 +50,7 @@
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css"> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
<!-- webpack bundles head --> <!-- webpack bundles head -->
<title>Prowlarr (Preview)</title> <title>Prowlarr</title>
<!-- <!--
The super basic styling for .root will live here, The super basic styling for .root will live here,
+54 -53
View File
@@ -25,107 +25,108 @@
"not chrome < 60" "not chrome < 60"
], ],
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "6.1.1", "@fortawesome/fontawesome-free": "6.2.1",
"@fortawesome/fontawesome-svg-core": "6.1.1", "@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.1.1", "@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.1.1", "@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.1.18", "@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.6", "@microsoft/signalr": "6.0.11",
"@sentry/browser": "6.19.2", "@sentry/browser": "7.28.0",
"@sentry/integrations": "6.19.2", "@sentry/integrations": "7.28.0",
"chart.js": "3.7.1", "chart.js": "4.1.1",
"classnames": "2.3.1", "classnames": "2.3.2",
"clipboard": "2.0.10", "clipboard": "2.0.11",
"connected-react-router": "6.9.1", "connected-react-router": "6.9.3",
"element-class": "0.2.2", "element-class": "0.2.2",
"filesize": "6.3.0", "filesize": "10.0.6",
"history": "4.10.1", "history": "4.10.1",
"https-browserify": "1.0.0", "https-browserify": "1.0.0",
"jdu": "1.0.0", "jdu": "1.0.0",
"jquery": "3.6.0", "jquery": "3.6.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"mobile-detect": "1.4.5", "mobile-detect": "1.4.5",
"moment": "2.29.2", "moment": "2.29.4",
"mousetrap": "1.6.5", "mousetrap": "1.6.5",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"qs": "6.10.3", "qs": "6.11.0",
"react": "17.0.2", "react": "17.0.2",
"react-addons-shallow-compare": "15.6.3", "react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0", "react-async-script": "1.2.0",
"react-autosuggest": "10.1.0", "react-autosuggest": "10.1.0",
"react-custom-scrollbars-2": "4.4.0", "react-custom-scrollbars-2": "4.5.0",
"react-dnd": "14.0.4", "react-dnd": "14.0.4",
"react-dnd-html5-backend": "14.0.2", "react-dnd-html5-backend": "14.0.2",
"react-dnd-multi-backend": "6.0.2", "react-dnd-multi-backend": "6.0.2",
"react-dnd-touch-backend": "14.1.1", "react-dnd-touch-backend": "14.1.1",
"react-document-title": "2.0.3", "react-document-title": "2.0.3",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-focus-lock": "2.5.0", "react-focus-lock": "2.9.2",
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0", "react-lazyload": "3.2.0",
"react-measure": "1.4.7", "react-measure": "1.4.7",
"react-popper": "1.3.7", "react-popper": "1.3.7",
"react-redux": "7.2.4", "react-redux": "8.0.5",
"react-router": "5.2.0", "react-router": "5.2.0",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-text-truncate": "0.19.0",
"react-virtualized": "9.21.1", "react-virtualized": "9.21.1",
"redux": "4.1.0", "redux": "4.2.0",
"redux-actions": "2.6.5", "redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0", "redux-batched-actions": "0.5.0",
"redux-localstorage": "0.4.1", "redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0", "redux-thunk": "2.4.2",
"reselect": "4.0.0" "reselect": "4.1.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.18.2", "@babel/core": "7.20.5",
"@babel/eslint-parser": "7.18.2", "@babel/eslint-parser": "7.19.1",
"@babel/plugin-proposal-class-properties": "7.17.12", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.18.2", "@babel/plugin-proposal-decorators": "7.20.5",
"@babel/plugin-proposal-export-default-from": "7.17.12", "@babel/plugin-proposal-export-default-from": "7.18.10",
"@babel/plugin-proposal-export-namespace-from": "7.17.12", "@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-function-sent": "7.18.2", "@babel/plugin-proposal-function-sent": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-numeric-separator": "7.16.7", "@babel/plugin-proposal-numeric-separator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.17.12", "@babel/plugin-proposal-optional-chaining": "7.18.9",
"@babel/plugin-proposal-throw-expressions": "7.16.7", "@babel/plugin-proposal-throw-expressions": "7.18.6",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.18.2", "@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.17.12", "@babel/preset-react": "7.18.6",
"autoprefixer": "10.4.7", "autoprefixer": "10.4.13",
"babel-loader": "8.2.5", "babel-loader": "9.1.0",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.22.8", "core-js": "3.26.1",
"css-loader": "6.7.1", "css-loader": "6.7.3",
"eslint": "8.17.0", "eslint": "8.30.0",
"eslint-plugin-filenames": "1.3.2", "eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.30.0", "eslint-plugin-react": "7.31.11",
"eslint-plugin-simple-import-sort": "7.0.0", "eslint-plugin-simple-import-sort": "8.0.0",
"esprint": "3.6.0", "esprint": "3.6.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7", "filemanager-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0", "loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.6.0", "mini-css-extract-plugin": "2.7.2",
"postcss": "8.4.14", "postcss": "8.4.20",
"postcss-color-function": "4.1.0", "postcss-color-function": "4.1.0",
"postcss-loader": "6.2.1", "postcss-loader": "7.0.2",
"postcss-mixins": "9.0.2", "postcss-mixins": "9.0.4",
"postcss-nested": "5.0.6", "postcss-nested": "6.0.0",
"postcss-simple-vars": "6.0.3", "postcss-simple-vars": "7.0.1",
"postcss-url": "10.1.3", "postcss-url": "10.1.3",
"require-nocache": "1.0.0", "require-nocache": "1.0.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"run-sequence": "2.2.1", "run-sequence": "2.2.1",
"streamqueue": "1.1.2", "streamqueue": "1.1.2",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"stylelint": "14.8.5", "stylelint": "14.16.0",
"stylelint-order": "5.0.0", "stylelint-order": "5.0.0",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"webpack": "5.73.0", "webpack": "5.75.0",
"webpack-cli": "4.9.2", "webpack-cli": "5.0.1",
"webpack-livereload-plugin": "3.0.2" "webpack-livereload-plugin": "3.0.2"
} }
} }
+1 -1
View File
@@ -94,7 +94,7 @@
<!-- Standard testing packages --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start(true);
driver.Url = "http://localhost:9696"; driver.Url = "http://localhost:9696";
@@ -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();
}
}
}
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@@ -15,8 +16,11 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories; using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http namespace NzbDrone.Common.Test.Http
{ {
@@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost; private string _httpBinHost;
private string _httpBinHost2; private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp] [OneTimeSetUp]
public void FixtureSetUp() public void FixtureSetUp()
{ {
@@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com"; var mainHost = "httpbin.servarr.com";
// Use mirrors for tests that use two hosts // Use mirrors for tests that use two hosts
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" }; var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable. // httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHost = mainHost; _httpBinHost = mainHost;
@@ -46,31 +52,22 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available."); TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10; _httpBinSleep = 10;
} }
private bool IsTestSiteAvailable(string site) private bool IsTestSiteAvailable(string site)
{ {
try try
{ {
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest; var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK) if (res.StatusCode != HttpStatusCode.OK)
{ {
return false; return false;
} }
try res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429) if (res == null || res.StatusCode != HttpStatusCode.TooManyRequests)
{ {
return false; return false;
} }
@@ -95,10 +92,13 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS"); Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0"); Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>()); Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>()); Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>()); Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>()); Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>()); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>()); Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[TestCase(CertificateValidationType.Enabled)]
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test] [Test]
public void should_execute_typed_get() public void should_execute_typed_get()
{ {
@@ -162,15 +184,45 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message); response.Resource.Data.Should().Be(message);
} }
[TestCase("gzip")] [Test]
public void should_execute_get_using_gzip(string compression) public void should_execute_post_with_content_type()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/{compression}"); var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression); response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
response.Resource.Gzipped.Should().BeTrue(); response.Resource.Gzipped.Should().BeTrue();
response.Resource.Brotli.Should().BeFalse();
}
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
response.Resource.Gzipped.Should().BeFalse();
response.Resource.Brotli.Should().BeTrue();
} }
[TestCase(HttpStatusCode.Unauthorized)] [TestCase(HttpStatusCode.Unauthorized)]
@@ -190,6 +242,28 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test] [Test]
public void should_not_follow_redirects_when_not_in_production() public void should_not_follow_redirects_when_not_in_production()
{ {
@@ -315,13 +389,38 @@ namespace NzbDrone.Common.Test.Http
{ {
var file = GetTempFilePath(); var file = GetTempFilePath();
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file)); Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse(); File.Exists(file).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
[Test]
public void should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
using (var fileStream = new FileStream(file, FileMode.Create))
{
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
ExceptionVerification.ExpectedErrors(1);
File.Exists(file).Should().BeTrue();
var fileInfo = new FileInfo(file);
fileInfo.Length.Should().Be(0);
}
[Test] [Test]
public void should_send_cookie() public void should_send_cookie()
{ {
@@ -753,6 +852,7 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; } public string Url { get; set; }
public string Data { get; set; } public string Data { get; set; }
public bool Gzipped { get; set; } public bool Gzipped { get; set; }
public bool Brotli { get; set; }
} }
public class HttpCookieResource public class HttpCookieResource
@@ -1,4 +1,4 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Test.Common; 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://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) public void should_parse(string uri)
{ {
var newUri = new HttpUri(uri); var newUri = new HttpUri(uri);
@@ -98,15 +98,30 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Internal // Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")] [TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")] [TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_clean_message(string message) public void should_clean_message(string message)
{ {
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret"); cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210"); cleansedMessage.Should().NotContain("01233210");
} }
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
[TestCase(@"Some message (from 32.2.3.5 user agent)")] [TestCase(@"Some message (from 32.2.3.5 user agent)")]
[TestCase(@"Auth-Invalidated ip 32.2.3.5")] [TestCase(@"Auth-Invalidated ip 32.2.3.5")]
[TestCase(@"Auth-Success ip 32.2.3.5")] [TestCase(@"Auth-Success ip 32.2.3.5")]
@@ -7,34 +7,50 @@ namespace NzbDrone.Common.Extensions
{ {
public static bool IsLocalAddress(this IPAddress ipAddress) 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)) if (IPAddress.IsLoopback(ipAddress))
{ {
return true; return true;
} }
// IPv4
if (ipAddress.AddressFamily == AddressFamily.InterNetwork) if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
{ {
byte[] bytes = ipAddress.GetAddressBytes(); return IsLocalIPv4(ipAddress.GetAddressBytes());
switch (bytes[0]) }
{
case 10: // IPv6
case 127: if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
return true; {
case 172: return ipAddress.IsIPv6LinkLocal ||
return bytes[1] < 32 && bytes[1] >= 16; ipAddress.IsIPv6UniqueLocal ||
case 192: ipAddress.IsIPv6SiteLocal;
return bytes[1] == 168;
default:
return false;
}
} }
return false; 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();
}
} }
} }
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -210,5 +211,51 @@ namespace NzbDrone.Common.Extensions
return result.TrimStart(' ', '.').TrimEnd(' '); return result.TrimStart(' ', '.').TrimEnd(' ');
} }
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
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;
}
} }
} }
@@ -0,0 +1,12 @@
using System.Net;
namespace NzbDrone.Common.Http
{
public class BasicNetworkCredential : NetworkCredential
{
public BasicNetworkCredential(string user, string pass)
: base(user, pass)
{
}
}
}
+2 -1
View File
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Http
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should // NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)"); private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' }; private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' }; private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -24,7 +25,7 @@ namespace NzbDrone.Common.Http
var matches = _CookieRegex.Match(cookieHeader); var matches = _CookieRegex.Match(cookieHeader);
while (matches.Success) while (matches.Success)
{ {
if (matches.Groups.Count > 2) if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))
{ {
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value; cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
} }
@@ -0,0 +1,11 @@
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface ICertificateValidationService
{
bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
}
}
@@ -6,6 +6,5 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher public interface IHttpDispatcher
{ {
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies); Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
Task DownloadFileAsync(string url, string fileName);
} }
} }
@@ -1,13 +1,16 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NLog.Fluent; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
@@ -15,221 +18,225 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
public class ManagedHttpDispatcher : IHttpDispatcher public class ManagedHttpDispatcher : IHttpDispatcher
{ {
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy; private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder; private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo; private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly Logger _logger; private readonly ICached<CredentialCache> _credentialCache;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
{ {
_proxySettingsProvider = proxySettingsProvider; _proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy; _createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_userAgentBuilder = userAgentBuilder; _userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger; _httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
} }
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies) public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{ {
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
// Deflate is not a standard and could break depending on implementation. var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
// we should just stick with the more compatible Gzip if (cookieHeader.IsNotNullOrWhiteSpace())
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{ {
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); requestMessage.Headers.Add("Cookie", cookieHeader);
} }
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url); using var cts = new CancellationTokenSource();
if (request.RequestTimeout != TimeSpan.Zero)
{
cts.CancelAfter(request.RequestTimeout);
}
else
{
// The default for System.Net.Http.HttpClient
cts.CancelAfter(TimeSpan.FromSeconds(100));
}
if (request.Credentials != null)
{
if (request.Credentials is BasicNetworkCredential bc)
{
// Manually set header to avoid initial challenge response
var authInfo = bc.UserName + ":" + bc.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
}
else if (request.Credentials is NetworkCredential nc)
{
var creds = GetCredentialCache();
foreach (var authtype in new[] { "Basic", "Digest" })
{
creds.Remove((Uri)request.Url, authtype);
creds.Add((Uri)request.Url, authtype, nc);
}
}
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
if (request.Headers != null) if (request.Headers != null)
{ {
AddRequestHeaders(webRequest, request.Headers); AddRequestHeaders(requestMessage, request.Headers);
} }
HttpWebResponse httpWebResponse; var httpClient = GetClient(request.Url, request.ProxySettings);
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
try using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{ {
if (request.ContentData != null) byte[] data = null;
{
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
httpWebResponse = (HttpWebResponse)await webRequest.GetResponseAsync(); try
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{ {
// The default messages for WebException on mono are pretty horrible. if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{ {
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
}
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
} }
else else
{ {
throw; data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult();
} }
} }
} catch (Exception ex)
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
{ {
try throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
CookieContainer responseCookies = new CookieContainer();
if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieHeaders))
{
foreach (var responseCookieHeader in cookieHeaders)
{ {
data = await responseStream.ToBytes(); try
} {
catch (Exception ex) cookies.SetCookies(responseMessage.RequestMessage.RequestUri, responseCookieHeader);
{ }
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); catch
{
// Ignore invalid cookies
}
} }
} }
}
sw.Stop(); var cookieCollection = cookies.GetCookies(responseMessage.RequestMessage.RequestUri);
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, sw.ElapsedMilliseconds, httpWebResponse.StatusCode); sw.Stop();
}
public async Task DownloadFileAsync(string url, string fileName) return new HttpResponse(request, new HttpHeader(headers), cookieCollection, data, sw.ElapsedMilliseconds, responseMessage.StatusCode);
{
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
var uri = new HttpUri(url);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
await webClient.DownloadFileTaskAsync(url, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
}
catch (WebException e)
{
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
throw;
} }
} }
protected virtual IWebProxy GetProxy(HttpUri uri) protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri, HttpProxySettings requestProxy)
{ {
IWebProxy proxy = null; var proxySettings = requestProxy ?? _proxySettingsProvider.GetProxySettings(uri);
var proxySettings = _proxySettingsProvider.GetProxySettings(uri); var key = proxySettings?.Key ?? NO_PROXY_KEY;
return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings));
}
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
{
var handler = new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
UseCookies = false, // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false,
Credentials = GetCredentialCache(),
PreAuthenticate = true,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
}
};
if (proxySettings != null) if (proxySettings != null)
{ {
proxy = _createManagedWebProxy.GetWebProxy(proxySettings); handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
} }
return proxy; var client = new System.Net.Http.HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
} }
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers) protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
{ {
foreach (var header in headers) foreach (var header in headers)
{ {
switch (header.Key) switch (header.Key)
{ {
case "Accept": case "Accept":
webRequest.Accept = header.Value; webRequest.Headers.Accept.ParseAdd(header.Value);
break; break;
case "Connection": case "Connection":
webRequest.Connection = header.Value; webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
break; break;
case "Content-Length": case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value); AddContentHeader(webRequest, "Content-Length", header.Value);
break; break;
case "Content-Type": case "Content-Type":
webRequest.ContentType = header.Value; AddContentHeader(webRequest, "Content-Type", header.Value);
break; break;
case "Date": case "Date":
webRequest.Date = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Expect": case "Expect":
webRequest.Expect = header.Value; webRequest.Headers.Expect.ParseAdd(header.Value);
break; break;
case "Host": case "Host":
webRequest.Host = header.Value; webRequest.Headers.Host = header.Value;
break; break;
case "If-Modified-Since": case "If-Modified-Since":
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Range": case "Range":
throw new NotImplementedException(); throw new NotImplementedException();
case "Referer": case "Referer":
webRequest.Referer = header.Value; webRequest.Headers.Add("Referer", header.Value);
break; break;
case "Transfer-Encoding": case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value; webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
break; break;
case "User-Agent": case "User-Agent":
webRequest.UserAgent = header.Value; webRequest.Headers.UserAgent.Clear();
webRequest.Headers.UserAgent.ParseAdd(header.Value);
break; break;
case "Proxy-Connection": case "Proxy-Connection":
throw new NotImplementedException(); throw new NotImplementedException();
@@ -239,5 +246,84 @@ namespace NzbDrone.Common.Http.Dispatchers
} }
} }
} }
private void AddContentHeader(HttpRequestMessage request, string header, string value)
{
var headers = request.Content?.Headers;
if (headers == null)
{
return;
}
headers.Remove(header);
headers.Add(header, value);
}
private CredentialCache GetCredentialCache()
{
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
if (useIPv6)
{
try
{
var localToken = cancellationToken;
if (!hasResolvedIPv6Availability)
{
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
localToken = linkedTokenSource.Token;
}
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false;
}
finally
{
hasResolvedIPv6Availability = true;
}
}
// fallback to IPv4.
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
}
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
} }
} }
-15
View File
@@ -1,15 +0,0 @@
using System;
using System.Net;
namespace NzbDrone.Common.Http
{
public class GZipWebClient : WebClient
{
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
request.AutomaticDecompression = DecompressionMethods.GZip;
return request;
}
}
}
+127 -29
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -86,13 +87,21 @@ namespace NzbDrone.Common.Http
} }
// 302 or 303 should default to GET on redirect even if POST on original // 302 or 303 should default to GET on redirect even if POST on original
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod) if (RequestRequiresForceGet(response.StatusCode, response.Request.Method))
{ {
request.Method = HttpMethod.Get; request.Method = HttpMethod.Get;
request.ContentData = null; request.ContentData = null;
} }
response = await ExecuteRequestAsync(request, cookieContainer); // Save to add to final response
var responseCookies = response.Cookies;
// Update cookiecontainer for next request with any cookies recieved on last request
var responseContainer = HandleRedirectCookies(request, response);
response = await ExecuteRequestAsync(request, responseContainer);
response.Cookies.Add(responseCookies);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
} }
@@ -102,11 +111,14 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
} }
if (!request.SuppressHttpError && response.HasHttpError) if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
{ {
_logger.Warn("HTTP Error - {0}", response); if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
if ((int)response.StatusCode == 429) if (response.StatusCode == HttpStatusCode.TooManyRequests)
{ {
throw new TooManyRequestsException(request, response); throw new TooManyRequestsException(request, response);
} }
@@ -124,6 +136,21 @@ namespace NzbDrone.Common.Http
return ExecuteAsync(request).GetAwaiter().GetResult(); return ExecuteAsync(request).GetAwaiter().GetResult();
} }
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
switch (statusCode)
{
case HttpStatusCode.Moved:
case HttpStatusCode.Found:
case HttpStatusCode.MultipleChoices:
return requestMethod == HttpMethod.Post;
case HttpStatusCode.SeeOther:
return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
default:
return false;
}
}
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer) private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)
@@ -140,8 +167,6 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer); var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer); HandleResponseCookies(response, cookieContainer);
@@ -208,52 +233,125 @@ namespace NzbDrone.Common.Http
} }
} }
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer) private CookieContainer HandleRedirectCookies(HttpRequest request, HttpResponse response)
{ {
// Don't collect persistnet cookies for intermediate/redirected urls. var sourceContainer = new CookieContainer();
/*lock (_cookieContainerCache) var responseCookies = response.GetCookies();
if (responseCookies.Count != 0)
{ {
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); foreach (var pair in responseCookies)
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); {
var existingCookies = cookieContainer.GetCookies((Uri)request.Url); Cookie cookie;
if (pair.Value == null)
{
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
cookieContainer.Add(persistentCookies); sourceContainer.Add((Uri)request.Url, cookie);
cookieContainer.Add(existingCookies); }
}*/ }
return sourceContainer;
} }
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer) private void HandleResponseCookies(HttpResponse response, CookieContainer container)
{ {
foreach (Cookie cookie in container.GetAllCookies())
{
cookie.Expired = true;
}
var cookieHeaders = response.GetCookieHeaders(); var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty()) if (cookieHeaders.Empty())
{ {
return; return;
} }
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie) if (response.Request.StoreResponseCookie)
{ {
lock (_cookieContainerCache) lock (_cookieContainerCache)
{ {
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
foreach (var cookieHeader in cookieHeaders) AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
{ }
try }
{ }
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
} private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
catch (Exception ex) {
{ foreach (var cookieHeader in cookieHeaders)
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url); {
} try
} {
container.SetCookies((Uri)url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", url);
} }
} }
} }
public async Task DownloadFileAsync(string url, string fileName) public async Task DownloadFileAsync(string url, string fileName)
{ {
await _httpDispatcher.DownloadFileAsync(url, fileName); var fileNamePart = fileName + ".part";
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new HttpException(request, response, "Site responded with html content.");
}
}
stopWatch.Stop();
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.Move(fileNamePart, fileName);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
finally
{
if (File.Exists(fileNamePart))
{
File.Delete(fileNamePart);
}
}
} }
public void DownloadFile(string url, string fileName) public void DownloadFile(string url, string fileName)
+45
View File
@@ -4,11 +4,27 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public static class WebHeaderCollectionExtensions
{
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
{
var result = new NameValueCollection();
foreach (var header in headers)
{
result.Add(header.Key, header.Value.ConcatToString(";"));
}
return result;
}
}
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
{ {
public HttpHeader(NameValueCollection headers) public HttpHeader(NameValueCollection headers)
@@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http
{ {
} }
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
public HttpHeader() public HttpHeader()
{ {
} }
@@ -107,6 +128,30 @@ namespace NzbDrone.Common.Http
} }
} }
public string ContentEncoding
{
get
{
return GetSingleValue("Content-Encoding");
}
set
{
SetSingleValue("Content-Encoding", value);
}
}
public string Vary
{
get
{
return GetSingleValue("Vary");
}
set
{
SetSingleValue("Vary", value);
}
}
public string UserAgent public string UserAgent
{ {
get get
+9 -8
View File
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
@@ -12,11 +14,13 @@ namespace NzbDrone.Common.Http
{ {
public HttpRequest(string url, HttpAccept httpAccept = null) public HttpRequest(string url, HttpAccept httpAccept = null)
{ {
Method = HttpMethod.Get;
Url = new HttpUri(url); Url = new HttpUri(url);
Headers = new HttpHeader(); Headers = new HttpHeader();
Method = HttpMethod.Get; Method = HttpMethod.Get;
ConnectionKeepAlive = true; ConnectionKeepAlive = true;
AllowAutoRedirect = true; AllowAutoRedirect = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction) if (!RuntimeInfo.IsProduction)
@@ -34,19 +38,23 @@ namespace NzbDrone.Common.Http
public HttpMethod Method { get; set; } public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; } public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; } public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; } public HttpProxySettings ProxySettings { get; set; }
public byte[] ContentData { get; set; } public byte[] ContentData { get; set; }
public string ContentSummary { get; set; } public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; } public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString() public override string ToString()
{ {
@@ -103,12 +111,5 @@ namespace NzbDrone.Common.Http
return encoding.GetString(ContentData); return encoding.GetString(ContentData);
} }
} }
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
Headers.Set("Authorization", "Basic " + authInfo);
}
} }
} }
+9 -12
View File
@@ -21,12 +21,13 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; } public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; } public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; } public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
public NetworkCredential NetworkCredential { get; set; } public ICredentials NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; } public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
@@ -46,6 +47,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader(); Headers = new HttpHeader();
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>(); FormData = new List<HttpFormData>();
LogHttpError = true;
} }
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null) public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -106,6 +108,7 @@ namespace NzbDrone.Common.Http
request.Method = Method; request.Method = Method;
request.Encoding = Encoding; request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError; request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent; request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect; request.AllowAutoRedirect = AllowAutoRedirect;
request.StoreRequestCookie = StoreRequestCookie; request.StoreRequestCookie = StoreRequestCookie;
@@ -113,13 +116,7 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive; request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit; request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent; request.LogResponseContent = LogResponseContent;
request.Credentials = NetworkCredential;
if (NetworkCredential != null)
{
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers.Set("Authorization", "Basic " + authInfo);
}
foreach (var header in Headers) foreach (var header in Headers)
{ {
@@ -212,7 +209,7 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.UTF8.GetString(formData.ContentData)); summary.AppendFormat("\r\n{0}={1}", formData.Name, Encoding.GetString(formData.ContentData));
} }
} }
@@ -232,9 +229,9 @@ namespace NzbDrone.Common.Http
} }
else else
{ {
var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.UTF8.GetString(v.ContentData)))); var parameters = FormData.Select(v => string.Format("{0}={1}", v.Name, Uri.EscapeDataString(Encoding.GetString(v.ContentData))));
var urlencoded = string.Join("&", parameters); var urlencoded = string.Join("&", parameters);
var body = Encoding.UTF8.GetBytes(urlencoded); var body = Encoding.GetBytes(urlencoded);
request.Headers.ContentType = "application/x-www-form-urlencoded"; request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(body); request.SetContent(body);
@@ -406,7 +403,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData FormData.Add(new HttpFormData
{ {
Name = key, Name = key,
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)) ContentData = Encoding.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
}); });
return this; return this;
+7 -6
View File
@@ -64,10 +64,11 @@ namespace NzbDrone.Common.Http
public bool HasHttpError => (int)StatusCode >= 400; public bool HasHttpError => (int)StatusCode >= 400;
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved || public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.Found || StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.SeeOther ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.MultipleChoices ||
StatusCode == HttpStatusCode.PermanentRedirect ||
Headers.ContainsKey("Refresh"); Headers.ContainsKey("Refresh");
public string RedirectUrl public string RedirectUrl
@@ -88,13 +89,13 @@ namespace NzbDrone.Common.Http
if (match.Success) if (match.Success)
{ {
return (Request.Url += new HttpUri(match.Groups[2].Value)).FullUri; return (Request.Url + new HttpUri(match.Groups[2].Value)).FullUri;
} }
return string.Empty; return string.Empty;
} }
return (Request.Url += new HttpUri(newUrl)).FullUri; return (Request.Url + new HttpUri(newUrl)).FullUri;
} }
} }
@@ -117,7 +118,7 @@ namespace NzbDrone.Common.Http
public override string ToString() public override string ToString()
{ {
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode); var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{ {
+4 -2
View File
@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
{ {
public class HttpUri : IEquatable<HttpUri> 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; private readonly string _uri;
public string FullUri => _uri; public string FullUri => _uri;
@@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http
private void Parse() private void Parse()
{ {
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
var match = RegexUri.Match(_uri); var match = RegexUri.Match(_uri);
var scheme = match.Groups["scheme"]; var scheme = match.Groups["scheme"];
@@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http
var query = match.Groups["query"]; var query = match.Groups["query"];
var fragment = match.Groups["fragment"]; 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); throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
} }
@@ -1,4 +1,4 @@
using System; using System;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
@@ -25,5 +25,11 @@ namespace NzbDrone.Common.Http
} }
} }
} }
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
: base(request, response)
{
RetryAfter = retryWait;
}
} }
} }
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http
{
public class XmlRpcRequestBuilder : HttpRequestBuilder
{
public static string XmlRpcContentType = "text/xml";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
public string XmlMethod { get; private set; }
public List<object> XmlParameters { get; private set; }
public XmlRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
XmlParameters = new List<object>();
}
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
: this(BuildBaseUrl(useHttps, host, port, urlBase))
{
}
public override HttpRequestBuilder Clone()
{
var clone = base.Clone() as XmlRpcRequestBuilder;
clone.XmlParameters = new List<object>(XmlParameters);
return clone;
}
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
{
var clone = Clone() as XmlRpcRequestBuilder;
clone.XmlMethod = method;
clone.XmlParameters = parameters.ToList();
return clone;
}
protected override void Apply(HttpRequest request)
{
base.Apply(request);
request.Headers.ContentType = XmlRpcContentType;
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
if (XmlParameters.Any())
{
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
var paramsElement = new XElement("params", argElements);
methodCallElements.Add(paramsElement);
}
var message = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("methodCall", methodCallElements));
var body = message.ToString();
Logger.Debug($"Executing remote method: {XmlMethod}");
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
request.SetContent(body);
}
private static XElement ConvertParameter(object value)
{
XElement data;
if (value is string s)
{
data = new XElement("string", s);
}
else if (value is List<string> l)
{
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
}
else if (value is int i)
{
data = new XElement("int", i);
}
else if (value is byte[] bytes)
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
}
return new XElement("value", data);
}
}
}
@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -11,26 +11,41 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{ {
try try
{ {
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message); if (sentryEvent.Message is not null)
{
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
{
string str => str,
_ => x.ToString()
})).ToList();
}
if (sentryEvent.Fingerprint != null) if (sentryEvent.Fingerprint.Any())
{ {
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList(); var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
sentryEvent.SetFingerprint(fingerprint); sentryEvent.SetFingerprint(fingerprint);
} }
if (sentryEvent.Extra != null) if (sentryEvent.Extra.Any())
{ {
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value)); var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
sentryEvent.SetExtras(extras); sentryEvent.SetExtras(extras);
} }
foreach (var exception in sentryEvent.SentryExceptions) if (sentryEvent.SentryExceptions is not null)
{ {
exception.Value = CleanseLogMessage.Cleanse(exception.Value); foreach (var exception in sentryEvent.SentryExceptions)
foreach (var frame in exception.Stacktrace.Frames)
{ {
frame.FileName = ShortenPath(frame.FileName); exception.Value = CleanseLogMessage.Cleanse(exception.Value);
if (exception.Stacktrace is not null)
{
foreach (var frame in exception.Stacktrace.Frames)
{
frame.FileName = ShortenPath(frame.FileName);
}
}
} }
} }
} }
@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
"UnauthorizedAccessException", "UnauthorizedAccessException",
// Filter out people stuck in boot loops // Filter out people stuck in boot loops
"CorruptDatabaseException", "CorruptDatabaseException"
// This also filters some people in boot loops
"TinyIoCResolutionException"
}; };
public static readonly List<string> FilteredExceptionMessages = new List<string> public static readonly List<string> FilteredExceptionMessages = new List<string>
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Dsn = dsn; o.Dsn = dsn;
o.AttachStacktrace = true; o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200; o.MaxBreadcrumbs = 200;
o.SendDefaultPii = false;
o.Debug = false;
o.DiagnosticLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release; o.Release = BuildInfo.Release;
o.BeforeSend = x => SentryCleanser.CleanseEvent(x); o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null) if (ex != null)
{ {
fingerPrint.Add(ex.GetType().FullName); fingerPrint.Add(ex.GetType().FullName);
fingerPrint.Add(ex.TargetSite.ToString()); if (ex.TargetSite != null)
{
fingerPrint.Add(ex.TargetSite.ToString());
}
if (ex.InnerException != null) if (ex.InnerException != null)
{ {
fingerPrint.Add(ex.InnerException.GetType().FullName); fingerPrint.Add(ex.InnerException.GetType().FullName);
+8 -9
View File
@@ -4,19 +4,18 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" /> <PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="Sentry" Version="3.18.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" /> <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
@@ -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; }
}
}
@@ -0,0 +1,4 @@
{
"status": "success",
"response": []
}
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,7 @@
<subcat id="5030" name="SD"/> <subcat id="5030" name="SD"/>
<subcat id="5060" name="Sport"/> <subcat id="5060" name="Sport"/>
<subcat id="5010" name="WEB-DL"/> <subcat id="5010" name="WEB-DL"/>
<subcat id="5999" name="Other"/>
</category> </category>
<category id="7000" name="Other"> <category id="7000" name="Other">
<subcat id="7010" name="Misc"/> <subcat id="7010" name="Misc"/>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,144 @@
{
"status": "success",
"response": {
"currentPage": 1,
"pages": 1,
"results": [
{
"groupId": 2497,
"groupName": "Singin&#39; in the Rain",
"artist": "Gene Kelly & Stanley Donen",
"cover": "https:\/\/www.themoviedb.org\/t\/p\/original\/g2AaJDC2vSRcqHSDH29642xmQd.jpg",
"tags": [ "comedy", "musical", "romance" ],
"bookmarked": false,
"vanityHouse": false,
"groupYear": 1952,
"releaseType": null,
"groupTime": "1671129449",
"maxSize": 57473058680,
"totalSnatched": 25,
"totalSeeders": 9,
"totalLeechers": 0,
"torrents": [
{
"torrentId": 3599,
"editionId": 1,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "1080p",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 1,
"time": "2017-09-10 11:47:27",
"size": 24724893991,
"snatches": 14,
"seeders": 1,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
},
{
"torrentId": 45068,
"editionId": 2,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "2160p",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 1,
"time": "2022-12-15 19:37:29",
"size": 57473058680,
"snatches": 6,
"seeders": 8,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
},
{
"torrentId": 2726,
"editionId": 3,
"artists": [
{
"id": 126,
"name": "Gene Kelly",
"aliasid": 127
},
{
"id": 125,
"name": "Stanley Donen",
"aliasid": 126
}
],
"remastered": false,
"remasterYear": 0,
"remasterCatalogueNumber": "",
"remasterTitle": "",
"media": "DVD-R",
"encoding": "",
"format": "",
"hasLog": false,
"logScore": 0,
"hasCue": false,
"scene": false,
"vanityHouse": false,
"fileCount": 37,
"time": "2017-08-26 14:58:58",
"size": 10350032896,
"snatches": 5,
"seeders": 0,
"leechers": 0,
"isFreeleech": true,
"isNeutralLeech": false,
"isPersonalFreeleech": false,
"canUseToken": false,
"hasSnatched": false
}
]
}
]
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<caps>
<server version="1.0" title="Anime Tosho" strapline="Anime NZB/DDL mirror" url="https://animetosho.org/"/>
<limits max="200" default="75"/>
<retention days="9999"/>
<registration available="no" open="yes" />
<searching>
<search available="yes" supportedParams="q" />
<tv-search available="no" supportedParams="q" />
<movie-search available="no" supportedParams="q" />
</searching>
<categories>
<category id="5070" name="Anime" description="Anime"/>
</categories>
</caps>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
<channel>
<item>
<title>Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv</title>
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836164</guid>
<link>https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)</link>
<comments>https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836164</comments>
<pubDate>Tue, 20 Dec 2022 21:32:17 +0000</pubDate>
<size>5412993028</size>
<files>1</files>
<grabs>2</grabs>
<category>2000</category>
<category>2040</category>
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
<enclosure url="https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)" length="103641" type="application/x-bittorrent" />
<torznab:attr name="size" value="5412993028" />
<torznab:attr name="poster" value="anon" />
<torznab:attr name="seeders" value="3" />
<torznab:attr name="leechers" value="0" />
<torznab:attr name="peers" value="3" />
<torznab:attr name="infohash" value="(removed)" />
<torznab:attr name="downloadvolumefactor" value="1" />
<torznab:attr name="uploadvolumefactor" value="1" />
<torznab:attr name="tag" value="anonymous" />
<torznab:attr name="imdb" value="0039689" />
<torznab:attr name="imdbid" value="tt0039689" />
</item>
<item>
<title>Out of the Past 1947 1080p USA Blu-ray AVC DTS-HD MA 2.0-PCH</title>
<guid isPermaLink="true">https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836165</guid>
<link>https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)</link>
<comments>https://www.morethantv.me/torrents.php?id=(removed)&amp;torrentid=836165</comments>
<pubDate>Tue, 20 Dec 2022 21:47:40 +0000</pubDate>
<size>30524085127</size>
<files>78</files>
<grabs>0</grabs>
<category>2000</category>
<category>2040</category>
<description>A private eye escapes his past to run a gas station in a small town, but his past catches up with him. Now he must return to the big city world of danger, corruption, double crosses, and duplicitous dames.</description>
<enclosure url="https://www.morethantv.me/torrents.php?action=download&amp;id=(removed)&amp;authkey=(removed)&amp;torrent_pass=(removed)" length="150224" type="application/x-bittorrent" />
<torznab:attr name="size" value="30524085127" />
<torznab:attr name="poster" value="anon" />
<torznab:attr name="seeders" value="1" />
<torznab:attr name="leechers" value="0" />
<torznab:attr name="peers" value="1" />
<torznab:attr name="infohash" value="(removed)" />
<torznab:attr name="downloadvolumefactor" value="1" />
<torznab:attr name="uploadvolumefactor" value="1" />
<torznab:attr name="tag" value="anonymous" />
<torznab:attr name="imdb" value="0039689" />
<torznab:attr name="imdbid" value="tt0039689" />
</item>
</channel>
</rss>
+3 -1
View File
@@ -11,6 +11,7 @@ using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http; using NzbDrone.Core.Http;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
@@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>())); Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger)); Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder()); Mocker.SetConstant<IProwlarrCloudRequestBuilder>(new ProwlarrCloudRequestBuilder());
} }
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_warning_when_branch_not_valid() public void should_return_warning_when_branch_not_valid()
{ {
GivenValidBranch("master"); GivenValidBranch("test");
Subject.Check().ShouldBeWarning(); Subject.Check().ShouldBeWarning();
} }
[TestCase("Develop")]
[TestCase("develop")]
[TestCase("nightly")] [TestCase("nightly")]
[TestCase("Nightly")] [TestCase("Nightly")]
[TestCase("develop")]
[TestCase("master")]
public void should_return_no_warning_when_branch_valid(string branch) public void should_return_no_warning_when_branch_valid(string branch)
{ {
GivenValidBranch(branch); GivenValidBranch(branch);
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.History;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerStats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerStatsTests
{
public class IndexerStatisticsServiceFixture : CoreTest<IndexerStatisticsService>
{
private IndexerDefinition _indexer;
[SetUp]
public void Setup()
{
_indexer = Builder<IndexerDefinition>.CreateNew().With(x => x.Id = 5).Build();
Mocker.GetMock<IIndexerFactory>()
.Setup(o => o.All())
.Returns(new List<IndexerDefinition> { _indexer });
}
[Test]
public void should_pull_stats_if_all_events_are_failures()
{
var history = new List<History.History>
{
new History.History
{
Date = DateTime.UtcNow.AddHours(-1),
EventType = HistoryEventType.IndexerRss,
Successful = false,
Id = 8,
IndexerId = 5,
Data = new Dictionary<string, string> { { "source", "prowlarr" } }
}
};
Mocker.GetMock<IHistoryService>()
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
.Returns<DateTime, DateTime>((s, f) => history);
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
statistics.IndexerStatistics.Count.Should().Be(1);
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
}
}
}
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel"); torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 23:26:21")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-15 04:26:21"));
torrentInfo.Size.Should().Be(935127615); torrentInfo.Size.Should().Be(935127615);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2"); torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
@@ -60,6 +60,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.ImdbId.Should().Be(15569106); torrentInfo.ImdbId.Should().Be(15569106);
torrentInfo.TmdbId.Should().Be(135144); torrentInfo.TmdbId.Should().Be(135144);
torrentInfo.TvdbId.Should().Be(410548); torrentInfo.TvdbId.Should().Be(410548);
torrentInfo.Languages.Should().HaveCount(1);
torrentInfo.Languages.First().Should().Be("Japanese");
torrentInfo.Subs.Should().HaveCount(27);
torrentInfo.Subs.First().Should().Be("Arabic");
} }
} }
} }

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