Compare commits

...

104 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
278 changed files with 10411 additions and 3487 deletions
+2 -2
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.9' 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'
@@ -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
@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
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';
@@ -133,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
}; };
} }
@@ -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;
} }
@@ -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
}));
} }
}; };
} }
@@ -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>
@@ -14,8 +14,6 @@ function createLanguagesSelector() {
return createSelector( return createSelector(
(state) => state.localization, (state) => state.localization,
(localization) => { (localization) => {
console.log(localization);
const items = localization.items; const items = localization.items;
if (!items) { if (!items) {
@@ -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')
}, },
@@ -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,6 +4,7 @@ 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';
@@ -11,6 +12,7 @@ import indexerProxies from './Settings/indexerProxies';
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';
@@ -32,6 +34,7 @@ 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,
@@ -61,6 +64,7 @@ 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,
@@ -81,6 +85,7 @@ 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,
+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
}; };
+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();
}
}
}
@@ -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);
@@ -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;
@@ -231,5 +232,30 @@ namespace NzbDrone.Common.Extensions
.Replace("'", "%27") .Replace("'", "%27")
.Replace("%7E", "~"); .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;
}
} }
} }
+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;
}
} }
} }
@@ -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 -8
View File
@@ -4,18 +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="5.2.2" /> <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="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Sentry" Version="3.21.0" /> <PackageReference Include="Sentry" Version="3.24.1" />
<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; }
}
}
@@ -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>
@@ -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);
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex"); torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
torrentInfo.Size.Should().Be(7085405541); torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf"); torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton"); torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
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-03-21 00:24:49")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
torrentInfo.Size.Should().Be(69914591044); torrentInfo.Size.Should().Be(69914591044);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2"); torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
result.Should().Be(expected); result.Should().Be(expected);
} }
[TestCase("{{ .Today.Year }}", "2022")] [TestCase("{{ .Today.Year }}")]
public void should_handle_variables_statements(string template, string expected) public void should_handle_variables_statements(string template)
{ {
var result = Subject.ApplyGoTemplateText(template, _variables); var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected); result.Should().Be(DateTime.Now.Year.ToString());
} }
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")] [TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873"); torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
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("2020-01-25 22:20:19")); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19").ToUniversalTime());
torrentInfo.Size.Should().Be(8300512414); torrentInfo.Size.Should().Be(8300512414);
torrentInfo.InfoHash.Should().Be(null); torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null);
@@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
@@ -70,6 +71,45 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
caps.LimitsMax.Value.Should().Be(60); caps.LimitsMax.Value.Should().Be(60);
} }
[Test]
public void should_map_different_categories()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
bookCats.Count.Should().Be(2);
bookCats.Should().Contain("8000");
}
[Test]
public void should_find_sub_categories_as_main_categories()
{
GivenCapsResponse(ReadAllText("Files/Indexers/Torznab/torznab_animetosho_caps.xml"));
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTrackerCatToNewznab("5070");
bookCats.Count.Should().Be(2);
bookCats.First().Id.Should().Be(5070);
}
[Test]
public void should_map_by_name_when_available()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
bookCats.Count.Should().Be(2);
bookCats.First().Id.Should().Be(5050);
}
[Test] [Test]
public void should_use_default_pagesize_if_missing() public void should_use_default_pagesize_if_missing()
{ {
@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
{
[TestFixture]
public class OrpheusFixture : CoreTest<Orpheus>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Orpheus",
Settings = new OrpheusSettings() { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_Orpheus()
{
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(65);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-08-08 2:07:39"));
torrentInfo.Size.Should().Be(68296866);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(0);
torrentInfo.Seeders.Should().Be(0);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
{
[TestFixture]
public class SecretCinemaFixture : CoreTest<SecretCinema>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "SecretCinema",
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_SecretCinema()
{
var recentFeed = ReadAllText(@"Files/Indexers/SecretCinema/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(3);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("Singin' in the Rain (1952) 2160p");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://secret-cinema.pw/torrents.php?action=download&useToken=0&id=45068");
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
torrentInfo.Size.Should().Be(57473058680);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(8);
torrentInfo.Seeders.Should().Be(8);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(0);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}
@@ -34,6 +34,10 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
}; };
_caps = new IndexerCapabilities(); _caps = new IndexerCapabilities();
_caps.Categories.AddCategoryMapping(2000, NewznabStandardCategory.Movies, "Movies");
_caps.Categories.AddCategoryMapping(2040, NewznabStandardCategory.MoviesHD, "Movies/HD");
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps); .Returns(_caps);
@@ -129,6 +133,38 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
releaseInfo.Peers.Should().BeNull(); releaseInfo.Peers.Should().BeNull();
} }
[Test]
public async Task should_parse_recent_feed_from_torznab_morethantv()
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_morethantv.xml");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
var releaseInfo = releases.First() as TorrentInfo;
releaseInfo.Title.Should().Be("Out of the Past 1947 720p BluRay FLAC2.0 x264-CtrlHD.mkv");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
releaseInfo.DownloadUrl.Should().Be("https://www.morethantv.me/torrents.php?action=download&id=(removed)&authkey=(removed)&torrent_pass=(removed)");
releaseInfo.InfoUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
releaseInfo.CommentUrl.Should().Be("https://www.morethantv.me/torrents.php?id=(removed)&torrentid=836164");
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 20 Dec 2022 21:32:17 +0000").ToUniversalTime());
releaseInfo.Size.Should().Be(5412993028);
releaseInfo.TvdbId.Should().Be(0);
releaseInfo.TvRageId.Should().Be(0);
releaseInfo.InfoHash.Should().Be("(removed)");
releaseInfo.Seeders.Should().Be(3);
releaseInfo.Peers.Should().Be(3);
releaseInfo.Categories.Count().Should().Be(4);
}
[Test] [Test]
public void should_use_pagesize_reported_by_caps() public void should_use_pagesize_reported_by_caps()
{ {
@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="12.0.1" /> <PackageReference Include="YamlDotNet" Version="12.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
@@ -21,7 +21,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Datastore\Migration\" />
</ItemGroup>
</Project> </Project>
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
var status = Subject.GetBlockedProviders().FirstOrDefault(); var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull(); status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue(); status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
} }
[Test] [Test]
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
var status = Subject.GetBlockedProviders().FirstOrDefault(); var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull(); status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue(); status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500); status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
} }
[Test] [Test]
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
status.Should().NotBeNull(); status.Should().NotBeNull();
origStatus.EscalationLevel.Should().Be(3); origStatus.EscalationLevel.Should().Be(3);
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500); status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
} }
} }
} }
@@ -25,7 +25,6 @@ namespace NzbDrone.Core.Test.UpdateTests
} }
[Test] [Test]
[Ignore("TODO No Updates On Server")]
public void finds_update_when_version_lower() public void finds_update_when_version_lower()
{ {
UseRealHttp(); UseRealHttp();
@@ -74,6 +74,8 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings); var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Lidarr
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings); var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Lidarr
{ {
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{ {
// Retain user fields not-affiliated with Prowlarr
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match // Update the indexer if it still has categories that match
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings); _lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
} }
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Lidarr
{ {
var cacheKey = $"{Settings.BaseUrl}"; var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First(); var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First(); var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Lidarr
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,
Fields = schema.Fields, Fields = new List<LidarrField>()
}; };
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +202,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{ {
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
} }
} }
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public string Implementation { get; set; } public string Implementation { get; set; }
public string ConfigContract { get; set; } public string ConfigContract { get; set; }
public string InfoLink { get; set; } public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public List<LidarrField> Fields { get; set; } public List<LidarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath; var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
@@ -75,6 +75,8 @@ namespace NzbDrone.Core.Applications.Mylar
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings); var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Radarr
var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings); var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -126,6 +128,12 @@ namespace NzbDrone.Core.Applications.Radarr
{ {
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{ {
// Retain user fields not-affiliated with Prowlarr
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match // Update the indexer if it still has categories that match
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings); _radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
} }
@@ -159,6 +167,7 @@ namespace NzbDrone.Core.Applications.Radarr
{ {
var cacheKey = $"{Settings.BaseUrl}"; var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First(); var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First(); var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +184,11 @@ namespace NzbDrone.Core.Applications.Radarr
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,
Fields = schema.Fields, Fields = new List<RadarrField>()
}; };
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
public string Implementation { get; set; } public string Implementation { get; set; }
public string ConfigContract { get; set; } public string ConfigContract { get; set; }
public string InfoLink { get; set; } public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public List<RadarrField> Fields { get; set; } public List<RadarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath; var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Readarr
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings); var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Readarr
{ {
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{ {
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match // Update the indexer if it still has categories that match
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings); _readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
} }
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Readarr
{ {
var cacheKey = $"{Settings.BaseUrl}"; var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First(); var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First(); var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Readarr
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,
Fields = schema.Fields, Fields = new List<ReadarrField>()
}; };
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +198,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null) if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{ {
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
} }
} }
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath; var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Sonarr
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings); var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -126,6 +128,13 @@ namespace NzbDrone.Core.Applications.Sonarr
{ {
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any()) if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
{ {
// Retain user fields not-affiliated with Prowlarr
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
// Update the indexer if it still has categories that match // Update the indexer if it still has categories that match
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings); _sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
} }
@@ -159,6 +168,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{ {
var cacheKey = $"{Settings.BaseUrl}"; var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First(); var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First(); var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +185,11 @@ namespace NzbDrone.Core.Applications.Sonarr
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,
Fields = schema.Fields, Fields = new List<SonarrField>()
}; };
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -189,7 +201,7 @@ namespace NzbDrone.Core.Applications.Sonarr
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime; sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
} }
return sonarrIndexer; return sonarrIndexer;
@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
public string Implementation { get; set; } public string Implementation { get; set; }
public string ConfigContract { get; set; } public string ConfigContract { get; set; }
public string InfoLink { get; set; } public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public List<SonarrField> Fields { get; set; } public List<SonarrField> Fields { get; set; }
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath; var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
@@ -91,6 +91,8 @@ namespace NzbDrone.Core.Applications.Whisparr
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings); var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id }); _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
} }
_logger.Trace("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
} }
public override void RemoveIndexer(int indexerId) public override void RemoveIndexer(int indexerId)
@@ -126,6 +128,8 @@ namespace NzbDrone.Core.Applications.Whisparr
{ {
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{ {
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match // Update the indexer if it still has categories that match
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings); _whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
} }
@@ -159,6 +163,7 @@ namespace NzbDrone.Core.Applications.Whisparr
{ {
var cacheKey = $"{Settings.BaseUrl}"; var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First(); var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First(); var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +180,11 @@ namespace NzbDrone.Core.Applications.Whisparr
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,
Fields = schema.Fields, Fields = new List<WhisparrField>()
}; };
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath; var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Authentication
{
public enum AuthenticationRequiredType
{
Enabled = 0,
DisabledForLocalAddresses = 1
}
}
@@ -1,9 +1,10 @@
namespace NzbDrone.Core.Authentication namespace NzbDrone.Core.Authentication
{ {
public enum AuthenticationType public enum AuthenticationType
{ {
None = 0, None = 0,
Basic = 1, Basic = 1,
Forms = 2 Forms = 2,
External = 3
} }
} }
+3 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Authentication namespace NzbDrone.Core.Authentication
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
public Guid Identifier { get; set; } public Guid Identifier { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Salt { get; set; }
public int Iterations { get; set; }
} }
} }
@@ -1,4 +1,6 @@
using System; using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private static readonly int ITERATIONS = 10000;
private static readonly int SALT_SIZE = 128 / 8;
private static readonly int NUMBER_OF_BYTES = 256 / 8;
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{ {
_repo = repo; _repo = repo;
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
public User Add(string username, string password) public User Add(string username, string password)
{ {
return _repo.Insert(new User var user = new User
{ {
Identifier = Guid.NewGuid(), Identifier = Guid.NewGuid(),
Username = username.ToLowerInvariant(), Username = username.ToLowerInvariant()
Password = password.SHA256Hash() };
});
SetUserHashedPassword(user, password);
return _repo.Insert(user);
} }
public User Update(User user) public User Update(User user)
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
if (user.Password != password) if (user.Password != password)
{ {
user.Password = password.SHA256Hash(); SetUserHashedPassword(user, password);
} }
user.Username = username.ToLowerInvariant(); user.Username = username.ToLowerInvariant();
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
return null; return null;
} }
if (user.Password == password.SHA256Hash()) if (user.Salt.IsNullOrWhiteSpace())
{
// If password matches stored SHA256 hash, update to salted hash and verify.
if (user.Password == password.SHA256Hash())
{
SetUserHashedPassword(user, password);
return Update(user);
}
return null;
}
if (VerifyHashedPassword(user, password))
{ {
return user; return user;
} }
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
{ {
return _repo.FindUser(identifier); return _repo.FindUser(identifier);
} }
private User SetUserHashedPassword(User user, string password)
{
var salt = GenerateSalt();
user.Iterations = ITERATIONS;
user.Salt = Convert.ToBase64String(salt);
user.Password = GetHashedPassword(password, salt, ITERATIONS);
return user;
}
private byte[] GenerateSalt()
{
var salt = new byte[SALT_SIZE];
RandomNumberGenerator.Create().GetBytes(salt);
return salt;
}
private string GetHashedPassword(string password, byte[] salt, int iterations)
{
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: iterations,
numBytesRequested: NUMBER_OF_BYTES));
}
private bool VerifyHashedPassword(User user, string password)
{
var salt = Convert.FromBase64String(user.Salt);
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
return user.Password == hashedPassword;
}
} }
} }
@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration
bool EnableSsl { get; } bool EnableSsl { get; }
bool LaunchBrowser { get; } bool LaunchBrowser { get; }
AuthenticationType AuthenticationMethod { get; } AuthenticationType AuthenticationMethod { get; }
AuthenticationRequiredType AuthenticationRequired { get; }
bool AnalyticsEnabled { get; } bool AnalyticsEnabled { get; }
string LogLevel { get; } string LogLevel { get; }
string ConsoleLogLevel { get; } string ConsoleLogLevel { get; }
@@ -193,10 +194,12 @@ namespace NzbDrone.Core.Configuration
} }
} }
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
// TODO: Change back to "master" for the first stable release. // TODO: Change back to "master" for the first stable release.
public string Branch => GetValue("Branch", "develop").ToLowerInvariant(); public string Branch => GetValue("Branch", "master").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant(); public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
@@ -206,7 +209,7 @@ namespace NzbDrone.Core.Configuration
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false); public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false); public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false); public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string Theme => GetValue("Theme", "light", persist: false); public string Theme => GetValue("Theme", "auto", persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false); public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false); public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
var releaseInfoPath = Path.Combine(bin, "release_info"); var releaseInfoPath = Path.Combine(bin, "release_info");
PackageUpdateMechanism = UpdateMechanism.BuiltIn; PackageUpdateMechanism = UpdateMechanism.BuiltIn;
DefaultBranch = "develop"; DefaultBranch = "master";
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath)) if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
{ {
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Data; using System.Data;
using FluentMigrator; using FluentMigrator;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -21,6 +22,8 @@ namespace NzbDrone.Core.Datastore.Migration
cmd.Transaction = tran; cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'"; cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'";
var updatedIndexers = new List<Indexer008>();
using (var reader = cmd.ExecuteReader()) using (var reader = cmd.ExecuteReader())
{ {
while (reader.Read()) while (reader.Read())
@@ -45,19 +48,26 @@ namespace NzbDrone.Core.Datastore.Migration
// write new json back to db, switch to new ConfigContract, and disable the indexer // write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson(); settings = jsonObject.ToJson();
using (var updateCmd = conn.CreateCommand())
updatedIndexers.Add(new Indexer008
{ {
updateCmd.Transaction = tran; Id = id,
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?"; Settings = settings,
updateCmd.AddParameter(settings); ConfigContract = "RedactedSettings",
updateCmd.AddParameter("RedactedSettings"); Enable = false
updateCmd.AddParameter(id); });
updateCmd.ExecuteNonQuery();
}
} }
} }
} }
} }
} }
public class Indexer008
{
public int Id { get; set; }
public string Settings { get; set; }
public string ConfigContract { get; set; }
public bool Enable { get; set; }
}
} }
} }
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Data;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
using static NzbDrone.Core.Datastore.Migration.redacted_api;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(22)]
public class orpheus_api : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToRedactedApi);
}
private void MigrateToRedactedApi(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Orpheus'";
var updatedIndexers = new List<Indexer008>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = Json.Deserialize<JObject>(settings);
// Remove username
if (jsonObject.ContainsKey("username"))
{
jsonObject.Remove("username");
}
// Remove password
if (jsonObject.ContainsKey("password"))
{
jsonObject.Remove("password");
}
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
updatedIndexers.Add(new Indexer008
{
Id = id,
Settings = settings,
ConfigContract = "OrpheusSettings",
Enable = false
});
}
}
}
var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings, \"ConfigContract\" = @ConfigContract, \"Enable\" = @Enable WHERE \"Id\" = @Id";
conn.Execute(updateSql, updatedIndexers, transaction: tran);
}
}
}
}
@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(023)]
public class download_client_categories : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("DownloadClients")
.AddColumn("Categories").AsString().WithDefaultValue("[]");
}
}
}
@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(024)]
public class add_salt_to_users : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Users")
.AddColumn("Salt").AsString().Nullable()
.AddColumn("Iterations").AsInt32().Nullable();
}
}
}

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