Compare commits

...

141 Commits

Author SHA1 Message Date
PearsonFlyer
27064cd293 Fixed: Clarify IPT cookie help text (#421)
* Fixed: Clarify IPT cookie help text

* Update src/NzbDrone.Core/Indexers/Definitions/IPTorrents.cs

Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>

Co-authored-by: PearsonFlyer <PearsonFlyer@github.com>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2021-08-21 00:12:39 -04:00
Qstick
1fe8c63d41 Update README.md
[skip ci]
2021-08-20 23:45:24 -04:00
Qstick
b18e226718 Bump to 0.1.1 2021-08-20 23:29:08 -04:00
Qstick
682afc2c75 Fixed: (Rarbg) Blank baseUrl drop-down (#443)
* Fixed: (Rarbg) Blank baseUrl drop-down

* fixup! Using missing
2021-08-20 22:50:59 -04:00
Qstick
1ef43c40c0 Fixed: (BakaBT) Torrent downloads failing
Fixes #442
2021-08-19 23:37:14 -04:00
Robin Dadswell
b46e2c6ad1 New: Renamed Blacklist to Blocklist 2021-08-19 20:08:26 -04:00
Qstick
0a17b7e8ae Fixed: Send Link element in nab response 2021-08-18 22:23:22 -04:00
bakerboy448
b54c7e220e Fixed: Update AppIndexerRegex to support indexer ids over 100
Fixed Update AppIndexerRegex to support /api trailing (Mylar3)
2021-08-18 21:23:22 -04:00
bakerboy448
efb2a5751c Fixed: (Mylar3) Indexer Host needs to include trailing /api 2021-08-18 21:23:22 -04:00
Yukine
2cd0dde4e2 Fixed: (Indexer) AnimeBytes Synonymns & Links are optional 2021-08-17 18:50:32 -04:00
Weblate
de86274b08 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.0% (434 of 438 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2021-08-17 18:50:02 -04:00
Qstick
4a957b618e Fixed: Proxy HealthCheck should use Proxy.Test method
Fixes #431
2021-08-17 07:45:20 -04:00
Qstick
891ca0f56b Fixed: (Sonarr) Workaround Sonarr issue with caps and basic search
Fixes #430
2021-08-16 21:09:55 -04:00
Qstick
635fa78da9 Fixed: Cursor Jumps to end in when editing search term
Fixes #290
2021-08-15 22:41:33 -04:00
Qstick
b7731faedc Fixed: (Indexer Proxy) Socks4 and Socks5 not using Credentials 2021-08-15 19:48:14 -04:00
bakerboy448
7c1f5f769d Fixed: Settings Page Translates
New: Indexer Proxies Settings Page Links
- renamed Connect to Notifications
2021-08-15 09:15:07 -05:00
bakerboy448
e8c6103cc7 Fixed: Missing Indexer Proxy Modal translate 2021-08-15 09:15:07 -05:00
Qstick
742c0d02bc New: FlareSolverr Proxy Option 2021-08-15 01:57:00 -04:00
Qstick
7480ebea85 New: Per Indexer Proxies
Fixes #281
2021-08-15 01:57:00 -04:00
Qstick
31886e8d35 New: (Newznab) Parse PosterUrl when available 2021-08-15 00:42:51 -04:00
Qstick
252b9a1b6b Fixed: (Sonarr) Correctly set anime categories 2021-08-14 23:56:37 -04:00
Qstick
77892a3885 New: Sync Indexers with Mylar3 2021-08-14 23:50:47 -04:00
bakerboy448
09d839ffb1 Fixed: Incorrectly Cleansing TorrentLeech Search URL 2021-08-14 21:36:05 -04:00
Weblate
1c15932b90 Translated using Weblate (German)
Currently translated at 98.8% (421 of 426 strings)

Translated using Weblate (German)

Currently translated at 98.3% (419 of 426 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (426 of 426 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.4% (364 of 426 strings)

Co-authored-by: Nyuels <nils.faerber@rwth-aachen.de>
Co-authored-by: Nyuels <nyuels@nyuels.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Will Segatto <segatto.w@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2021-08-14 20:25:33 -04:00
Qstick
27c643d2f5 New: (DownloadClient) Aria2 2021-08-14 20:07:54 -04:00
Qstick
9a6391873f Fixed: (HDBits) Unknown Categories on every release
Fixes #365
2021-08-14 17:15:54 -04:00
Qstick
12ae8edc50 New: (Indexer) Binsearch 2021-08-14 15:39:46 -04:00
Qstick
868f779c5d Fixed: (Newznab) Don't die if TV or Music only
Fixes #283
2021-08-13 22:16:19 -04:00
Qstick
204052de9c Bump Dapper to 2.0.90 2021-08-13 21:46:57 -04:00
Qstick
878e269e70 Bump MailKit to 2.14.0 2021-08-13 21:42:26 -04:00
Qstick
1e317dac6b Bump Net5 to 5.0.9 2021-08-13 21:40:34 -04:00
Qstick
305df2fb7b Bump Microsoft.NET.Test.Sdk to 16.11.0 2021-08-13 21:38:31 -04:00
Qstick
3e07a9397c Bump DryIOC to 4.8.1 2021-08-13 21:34:39 -04:00
Qstick
3e2d3c510a Bump AngleSharp to 0.16.0 2021-08-13 21:31:13 -04:00
Qstick
bdcead007c Cleanup: (Rutracker) Purge unused using statements 2021-08-13 21:28:01 -04:00
Qstick
6c5d48621f Fixed: (Indexer) Use Indexer Encoding for Query and Auth
Fixes #370
2021-08-13 21:25:52 -04:00
Qstick
a95465195d New: (Indexer) BB 2021-08-13 18:20:40 -04:00
Qstick
5d5e2042d0 Fixed: Handle response when magnet is empty
Fixes #404
2021-08-13 16:46:48 -04:00
Qstick
5d980a175c Fixed: (DanishBytes) Handle response with null bumped_at
Fixes #384
2021-08-13 16:42:49 -04:00
Qstick
2114db02d8 Fixed: (Rarbg) Advertise IMDB, TMDB, TVDB Search
Fixes #392
2021-08-13 16:00:09 -04:00
Qstick
4bd23a60bd Fixed: Pad seasons parameter for newznab tvsearch 2021-08-13 15:37:25 -04:00
chryzsh
fe324dcc0c Fixed: (Indexer) PTP IMDB search 2021-08-13 07:42:21 -04:00
johnpyp
d5b34e8c03 Fixed: (Indexer) BroadcastheNet - Report TvdbId and RId 2021-08-09 22:40:14 -05:00
Qstick
e47c7e6a47 New: Add coverurl to Newznab response 2021-08-07 12:07:38 -04:00
Nyuels
d937e0324f Fixed: (Internet Archive) Follow redirects on grabs (#403) 2021-08-06 20:50:06 -04:00
Yukine
ff623d4c39 Fixed: (Indexer) AnimeBytes improve sonarr compatibility and make optional (#376)
* Fixed: (Indexer) AnimeBytes improve sonarr compatibility and make optional

* fix(AnimeBytes): correct numbering
2021-08-06 20:47:52 -04:00
Weblate
0b05f5ef24 Translated using Weblate (Arabic)
Currently translated at 1.1% (5 of 426 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (426 of 426 strings)

Translated using Weblate (Italian)

Currently translated at 84.2% (359 of 426 strings)

Translated using Weblate (Danish)

Currently translated at 13.6% (58 of 426 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Laubau <migdahs@gmail.com>
Co-authored-by: Simone <simoneungaro@hotmail.it>
Co-authored-by: bison529 <abshalsh@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2021-08-06 17:51:10 -04:00
Nyuels
e6c3292485 Fixed: (Tracker) Internet Archive: Add handling of missing result fields. (#402) 2021-08-06 13:53:58 -05:00
Nyuels
ba1c1baeb5 New: (Indexer) Internet Archive 2021-08-05 21:07:09 -04:00
Qstick
5e7f4f3fc1 Fixed: Add Test All for IndexerLongTermStatusCheck
Fixes #397
2021-08-05 13:47:30 -04:00
chryzsh
3dd11213fa Fixed: Gazelle search using full IMDb ID 2021-08-04 22:04:53 -04:00
bakerboy448
50cae0719f New: (Indexer) Alternative Links for TorrentDay
Fixes #386
2021-08-02 19:04:48 -05:00
crusher
2c6680e4fa New: (Indexer) Rutracker.org (#371)
* first attempt at sc definition

* add dologin. seems to return html instead of json

* barebones working setup

* added category but errors on override

* fixed and tested

* guid fix

* fixed bdmv and dvd releases to report in radarr accepted format

* New: (Indexer) RuTracker

* minor fixes

* somewhat working rutracker org defintion with some filtering

* filter russian letters option implemented

* deal with subtitle languages

* russian filtering pretty good now

* removed sc from branch

* rutracker handles series ok now

* final touches to rutracker

* tore out the captcha

Co-authored-by: Qstick <qstick@gmail.com>
2021-08-01 09:59:45 -04:00
Qstick
96afb7f327 Fixed: (BakaBT) Map AnimeMovie to Movie
Fixes #147
2021-08-01 09:59:44 -04:00
Qstick
0d1025d60a Fixed: (Cardigann) Input pairs in HandleRequest should be form data
Should fix some download links not working for Cardigann indexers
2021-08-01 09:59:44 -04:00
Qstick
0508dd2b66 Fixed: (Cardigann) Sites that use POST search not sending form params
Fixes #367
2021-08-01 09:59:44 -04:00
Weblate
026a503d5f Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.7% (425 of 426 strings)

Co-authored-by: Will Segatto <segatto.w@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2021-08-01 09:59:44 -04:00
Qstick
b3f8e648cd New: (Clients) Torrent and Usenet Blackhole
Fixes #238
2021-08-01 09:59:44 -04:00
Qstick
13b458090d New: RARBG tvdbId support and retry on failure
Fixes #333
2021-08-01 09:59:44 -04:00
Qstick
f97c3ff9bd Fixed: Application mapping regex fails 2021-08-01 09:59:43 -04:00
Qstick
841ff7b6ee Fixed: Don't fail sync on single app failure 2021-08-01 09:59:43 -04:00
bakerboy448
377db47daf git updates [skip ci] 2021-08-01 09:59:43 -04:00
crusher
2addcab765 New: (Indexer) - Secret Cinema (#372)
* first attempt at sc definition

* add dologin. seems to return html instead of json

* barebones working setup

* added category but errors on override

* fixed and tested

* guid fix

* fixed bdmv and dvd releases to report in radarr accepted format

* minor fixes

* fixed sc

* first attempt at SC categories

* category fixes for movies and music

* toggled back IsProduction
2021-08-01 09:59:31 -04:00
crusher
ddc676c608 Iptorrents tv episode search fix (#374)
* adjust search term for individual season in ipt
2021-08-01 09:59:01 -04:00
Qstick
924db7a394 New: (Indexer) TorrentParadiseMl 2021-07-29 21:23:23 -04:00
Qstick
580113d6ce New: (Indexer) TorrentSyndikat 2021-07-28 19:37:51 -04:00
Qstick
d532a69edc New: (Indexer) TorrentsCSV 2021-07-28 19:02:20 -04:00
Qstick
01f7e11d5a New: (Indexer) SceneTime 2021-07-27 22:28:07 -04:00
Qstick
de97ec95db Update YamlDotNet to 11.2.1 2021-07-26 21:55:46 -04:00
Qstick
6e66467ab3 Update Mailkit to 2.13.0 2021-07-26 21:52:59 -04:00
Qstick
4c5131708d Update RestSharp Packages 2021-07-26 21:48:47 -04:00
Qstick
bb2e1a6037 Update Sentry Packages 2021-07-26 21:46:04 -04:00
Qstick
5949bd97fd Update Lint Packages 2021-07-26 21:41:45 -04:00
Qstick
eaff071b16 Bump .Net5 to 5.0.8 2021-07-26 21:35:25 -04:00
Dmitry Chepurovskiy
a922586aba New: (Indexer) Anidub 2021-07-26 21:22:01 -04:00
Qstick
80beea9bdb Fixed: Don't return results with categories that were not searched 2021-07-25 22:58:31 -04:00
Qstick
8c326fc5c2 Fixed: (ExoticaZ) Update Categories 2021-07-25 21:38:21 -04:00
Dmitry Chepurovskiy
e26081acff New: (Indexer) Animedia (#360) 2021-07-24 11:37:28 -04:00
Ashino
b3b0467d22 New: (Indexer) Torrent Xthor (#342)
* New: (Indexer) Torrent Xthor

* New: (Indexer) Torrent Xthor

Fix multiple lines fieldDefinition and texts

* New: (Indexer) Torrent Xthor
Remove unwanted blankline

* New: (Indexer) Torrent Xthor
Fix BaseSettings after rebase

* New: (Indexer) Torrent Xthor

* New: (Indexer) Torrent Xthor

Fix multiple lines fieldDefinition and texts

* New: (Indexer) Torrent Xthor
Remove unwanted blankline

* New: (Indexer) Torrent Xthor
Fix BaseSettings after rebase

* New: (Indexer) Torrent Xthor
- Add "EnhancedFrenchAccent" field that will allow to find VF2 releases when searching VFF or VFQ
- Fix BaseSettings
- Decrease the RateLimit to 2.1 seconds
- Remove page argument when searching page 0

* Fix punctuation

* New: (Indexer) Torrent Xthor

* New: (Indexer) Torrent Xthor

Fix multiple lines fieldDefinition and texts

* New: (Indexer) Torrent Xthor
Remove unwanted blankline

* New: (Indexer) Torrent Xthor
Fix BaseSettings after rebase

* New: (Indexer) Torrent Xthor
- Add "EnhancedFrenchAccent" field that will allow to find VF2 releases when searching VFF or VFQ
- Fix BaseSettings
- Decrease the RateLimit to 2.1 seconds
- Remove page argument when searching page 0

* Fix punctuation

* New: (Indexer) Torrent Xthor
- Fix "unknown" categories when searching with Prowlarr (category conversion was missing)
- Now every class has its own file (like all others indexers)

* New: (Indexer) Torrent Xthor

- Fix fielddefinition to be on single line

Co-authored-by: TCLE <t.clemenceau@shinken-solutions.com>
2021-07-23 21:09:57 -04:00
Servarr
e1c98d2b38 Translated using Weblate (Portuguese (Brazil)) (#354)
Currently translated at 99.7% (425 of 426 strings)

Added translation using Weblate (Chinese (Min Nan))

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

Currently translated at 65.7% (280 of 426 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (426 of 426 strings)

Translated using Weblate (French)

Currently translated at 100.0% (426 of 426 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (French)

Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (German)

Currently translated at 98.1% (417 of 425 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: doob187 <amderkum@gmail.com>
Co-authored-by: foXaCe <foxace66@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Sean <zhangshuyan@fuji.waseda.jp>
Co-authored-by: doob187 <amderkum@gmail.com>
Co-authored-by: foXaCe <foxace66@gmail.com>
2021-07-21 10:49:00 -05:00
Qstick
e304461dfb New: Add PosterUrl to ReleaseInfo for parsing 2021-07-18 21:33:17 -04:00
Qstick
a127e5a30f Update and rename feature_request.md to feature_request.yml
[skip ci]
2021-07-17 11:51:03 -04:00
Qstick
e252cd4d3e Update bug_report.yml 2021-07-17 11:42:54 -04:00
Dmitry Chepurovskiy
9a1bd3db4c New: (Indexer) Shizaproject 2021-07-17 08:29:00 -04:00
Qstick
acce098e02 Update and rename bug_report.md to bug_report.yml 2021-07-17 08:14:36 -04:00
Yukine
afa87b7113 Fixed: (Anime Tosho) Only include Releases which have a NZB (#351) 2021-07-15 21:33:42 -04:00
bakerboy448
4cbd2cd8bc Fixed: Add Missing Search Translates 2021-07-15 21:23:39 -04:00
Yukine
e81d0f3e97 Fixed: (AnimeBytes) apply LinksUnionConverter to model (#353) 2021-07-15 21:22:54 -04:00
Hawks
34a6a0e0c9 Update indexer list text 2021-07-15 06:36:46 -05:00
Yukine
4254a05ea3 Fixed: (AnimeBytes) cleanup code, fix episode searching & improve Season matching (#329)
* refactor/fix(AnimeBytes): use data classes & fix season searching

* fix: only append epsisode when season was found

* feat: add Episode padding back for Sonarr compatibility

* fix: strip Epsiode number from request
2021-07-14 21:03:49 -04:00
Yukine
d32a94c14d Fixed: (SubsPlease) fix offset release time issues 2021-07-14 19:01:54 -04:00
ntldr0
6a9155bcf5 New: (Indexers) (Redacted) Add API support 2021-07-14 19:01:03 -04:00
Dmitry Chepurovskiy
de442cc659 New: (Indexer) Anilibria 2021-07-14 18:55:04 -04:00
bakerboy448
7f514c8f1e Fixed: Improved IPT's Cookie Help Text
ref https://www.reddit.com/r/prowlarr/comments/ojlamb/help_needed_with_ipt/h52nxnl/
2021-07-14 02:27:29 -05:00
bakerboy448
4c51f09acb Update bug report template [skip ci]
(cherry picked from commit 4659a8366d8a1565890d3b72442bd35c6eb8176e)
2021-07-13 20:55:43 +01:00
Qstick
a60388fcf9 Fixed: Indexer Info modal doesn't show correct Url 2021-07-12 23:26:17 -04:00
Qstick
11b656dabf Fixed: (TVVault) Advertise Indexer flags 2021-07-12 23:11:28 -04:00
Qstick
535f29bef4 Fixed: (DigitalCore) Release InfoUrls not being set 2021-07-12 23:11:28 -04:00
bakerboy448
0ddc530dd4 Fixed: Enhanced and Added Various C# Indexer Descriptions
Fixed: Consistency in C# Indexer Credential Fields
2021-07-12 21:57:27 -05:00
Servarr
b5321d33c9 Translated using Weblate (Catalan) (#336)
Currently translated at 1.1% (5 of 425 strings)

Added translation using Weblate (Catalan)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
2021-07-12 21:57:14 -05:00
Qstick
4116c10caa New: (Indexers) Per indexer api and download limits 2021-07-12 22:53:33 -04:00
Qstick
0fe2cf5c2d New: (Indexer) TV Vault 2021-07-12 22:03:51 -04:00
PearsonFlyer
c94573e868 Fixed: Respect categories for ImmortalSeed in search 2021-07-12 21:34:02 -04:00
bakerboy448
1945af060d Fixed: Provider Wiki Links (supported-X => supported#X) 2021-07-12 21:33:27 -04:00
Qstick
cf399ffdcc Fixed: (DanishBytes) Convert to use API
Fixes #158
2021-07-11 21:28:57 -04:00
Qstick
5846188202 Fixed: (IPTorrents) Correctly map WebDL 2021-07-11 20:51:59 -04:00
Yukine
fc65a89fbc refactor: improve readability, use UTC in request instead New York 2021-07-11 18:21:16 -04:00
bakerboy448
b62ae41de8 update pr temp [skip ci] 2021-07-10 15:12:00 -05:00
bakerboy448
8107f309b4 Update PULL_REQUEST_TEMPLATE.md 2021-07-10 11:34:11 -04:00
Qstick
c2c12297bd Fixed: Persist columns for search page 2021-07-08 20:49:15 -04:00
bakerboy448
81cbdab5eb New: (Indexer) SubsPlease Alt Links 2021-07-07 23:04:14 -04:00
Servarr
9ee5a3e94b Translated using Weblate (Hungarian) (#300)
Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.4% (363 of 425 strings)

Translated using Weblate (French)

Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (German)

Currently translated at 92.4% (393 of 425 strings)

Translated using Weblate (Portuguese)

Currently translated at 84.4% (359 of 425 strings)

Translated using Weblate (Portuguese)

Currently translated at 84.4% (359 of 425 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: foXaCe <foxace66@gmail.com>
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/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: foXaCe <foxace66@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
2021-07-07 00:55:40 -05:00
Qstick
a570fd2a8f New: Advanced settings toggle in indexer edit/add modal 2021-07-05 13:12:06 -04:00
Lagicrus
5cffb10e08 Fixed: If no categories are passed in, flag up a unknown error (#311)
* If no categories are passed in, flag up a unknown error

* Pass back in default props to deal with undefined issues
2021-07-05 08:42:51 -04:00
Qstick
b11bf284dc New: (IPTorrents) Add freeleech only option
Fixes #314
2021-07-05 08:41:29 -04:00
Robin Dadswell
5c4c042b2e Updated movieService reference for App Sync Profile Service dependency on Indexer Factory 2021-07-04 22:26:35 -04:00
Qstick
d1cb744efd Update azuresync.yml
[skip ci]
2021-07-01 21:59:49 -04:00
bakerboy448
79b910a80c Fixed: Cleanse BHD APIKey from logs 2021-07-01 16:22:57 -05:00
bakerboy448
01e08e0c31 fix radarr reference in contributing [skip ci] 2021-07-01 16:21:38 -05:00
Junkbite
dd3c9c268e allow empty catergories on indexer search 2021-06-30 21:57:53 -04:00
bakerboy448
725f738ee1 Fixed: Clarify redirect wording 2021-06-30 10:43:33 -05:00
Qstick
22c738f43e Prevent sync jobs from running in parallel
[skip ci]
2021-06-29 21:53:16 -04:00
Qstick
e45f88473c Fixed: (Anthelion) Null BaseUrl and better error message on auth fail
Fixes #295
2021-06-28 23:04:10 -04:00
Qstick
889591d0b1 Fixed: (HDSpace) Use query params 2021-06-28 22:43:30 -04:00
Qstick
fe8247df8a Fixed: (HDSpace) Auth failure and Name 2021-06-28 21:36:05 -04:00
bakerboy448
7a5721bcee typo fix 2021-06-28 20:26:34 -05:00
bakerboy448
eea5c3e9a4 Fixed: Cleanse Pwd from logs 2021-06-28 12:22:47 -05:00
Qstick
f55493c9a9 New: (Indexer) HD-Space 2021-06-27 21:40:43 -04:00
bakerboy448
79618adaf9 New: (Indexer) Add IPTorrents Alt Links
as of Jackett 4524c18d39251c677bec1d4d8f75f71c6c6ac010
2021-06-27 19:32:31 -04:00
Qstick
af13d6ed80 Fixed: Not using correct BaseUrl if changed between tests 2021-06-27 18:15:18 -04:00
Qstick
38c09277d9 New: Alternative Site Links 2021-06-27 01:58:37 -04:00
Qstick
1fb693d066 Update azuresync.yml
[skip ci]
2021-06-26 18:04:48 -04:00
Qstick
7414a2f690 Update azuresync.yml
[skip ci]
2021-06-26 17:59:12 -04:00
Qstick
000590bcf7 Update azuresync.yml
[skip ci]
2021-06-26 17:57:00 -04:00
Qstick
d5d6625a63 Update azuresync.yml
[skip ci]
2021-06-26 17:16:12 -04:00
Qstick
00f33cb48f Set area and sub-area on azure sync
[skip ci]
2021-06-26 17:09:23 -04:00
Qstick
fd265c5734 Update azure-pipelines.yml 2021-06-26 14:58:58 -04:00
Qstick
316543b9aa Change Azure to Agile Config
[skip ci]
2021-06-26 11:07:51 -04:00
Qstick
117ebcff2d Fix default azure board buckets
[skip ci]
2021-06-26 10:55:13 -04:00
Qstick
aab394b2c8 Update azuresync.yml
[skip ci]
2021-06-26 10:51:14 -04:00
Qstick
6ae520c061 Test Azure Boards sync 2021-06-26 10:47:23 -04:00
300 changed files with 14048 additions and 1727 deletions

View File

@@ -1,38 +0,0 @@
---
name: Bug Report
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit or Discord first. Exceptions do not mean you found a bug!
title: ''
labels: 'Type: Bug'
assignees: ''
---
<!-- Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug! -->
<!-- Note: Text between <!- and -> will be hidden -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen.-->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem.-->
**Platform Information (please complete the following information):**
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
- Docker: <!-- [Yes/No] -->
- .NET Version (System -> Status): <!--[e.g. .NET 5.0.1] -->
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
- Prowlarr Version: <!--[e.g. 0.1.2.1854-->
- Prowlarr Branch: <!--[e.g. develop, nightly]-->
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
<!-- Trace logs are named Prowlarr.trace.txt or Prowlarr.trace.#.txt and will contain "trace" in them-->
<!-- Please see the Wiki for how to provide proper and useful trace log files https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files -->

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Prowlarr**: Prowlarr 0.1.0.650
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Prowlarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Master
- Develop
- Nightly
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Trace Logs (https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

View File

@@ -1,20 +0,0 @@
---
name: Feature Request
about: Suggest an idea for Prowlarr
title: ''
labels: 'Type: Feature Request'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -0,0 +1,39 @@
name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Prowlarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

View File

@@ -1,5 +1,5 @@
#### Database Migration
YES | NO
YES - XXXX | NO
#### Description
A few sentences describing the overall goals of the pull request's commits.
@@ -8,8 +8,8 @@ A few sentences describing the overall goals of the pull request's commits.
#### Todos
- [ ] Tests
- [ ] Translation Keys
- [ ] Wiki Updates
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
- [ ] [Wiki Updates](https://wiki.servarr.com)
#### Issues Fixed or Closed by this PR

41
.github/workflows/azuresync.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Prowlarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Prowlarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100

View File

@@ -2,6 +2,8 @@
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/prowlarr/contributing).
## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better.
@@ -27,7 +29,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.serva
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Radarr's develop branch, don't merge
- Rebase from Prowlarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the discord if you have any questions

View File

@@ -11,13 +11,14 @@ Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base s
## Major Features Include:
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
- Torrent support for almost 500 trackers & more coming soon
- Torrent support for over 500 trackers with more added all the time
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required
- Indexer History and Statistics
- Manual Searching of Trackers & Indexers at a category level
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr/Mylar3, so no manual configuration of the other applications are required
- Indexer history and statistics
- Manual searching of Trackers & Indexers at a category level
- Support for pushing releases directly to your download clients from Prowlarr
- Indexer health and status notifications
- Per Indexer proxy support (SOCKS4, SOCKS5, HTTP, Flaresolverr)
## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
@@ -34,10 +35,6 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
[Indexer Requests](https://requests.prowlarr.com)
- Request or vote on an existing request for a new tracker/indexer
## Feature Requests
[Feature Requests](https://github.com/Prowlarr/Prowlarr/issues/new?assignees=&template=feature_request.md&Type%3A%20Feature%20Request&title=)
## Contributors & Developers
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>

View File

@@ -7,13 +7,13 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
majorVersion: '0.1.0'
majorVersion: '0.1.1'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.203'
dotnetVersion: '5.0.400'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
@@ -879,7 +879,7 @@ stages:
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none
- powershell: |
- pwsh: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

View File

@@ -11,6 +11,7 @@ import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSetti
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
@@ -90,6 +91,11 @@ function AppRoutes(props) {
component={Settings}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/applications"
component={ApplicationSettingsConnector}

View File

@@ -11,8 +11,6 @@ class InfoInput extends Component {
value
} = this.props;
console.log(this.props);
return (
<span dangerouslySetInnerHTML={{ __html: value }} />
);

View File

@@ -48,6 +48,10 @@ const links = [
title: translate('Settings'),
to: '/settings',
children: [
{
title: translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('Apps'),
to: '/settings/applications'

View File

@@ -228,4 +228,4 @@ export const UNSAVED_SETTING = farDotCircle;
export const VIEW = fasEye;
export const WARNING = fasExclamationTriangle;
export const WIKI = fasBookReader;
export const BLACKLIST = fasBan;
export const BLOCKLIST = fasBan;

View File

@@ -13,6 +13,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
@@ -31,6 +32,7 @@ function EditIndexerModalContent(props) {
onSavePress,
onTestPress,
onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps
} = props;
@@ -43,6 +45,7 @@ function EditIndexerModalContent(props) {
supportsRss,
supportsRedirect,
appProfileId,
tags,
fields,
priority
} = item;
@@ -87,7 +90,6 @@ function EditIndexerModalContent(props) {
type={inputTypes.CHECK}
name="enable"
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enable}
onChange={onInputChange}
/>
@@ -150,6 +152,18 @@ function EditIndexerModalContent(props) {
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText="Use tags to specify default clients, specify Indexer Proxies, or just to organize your indexers."
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
@@ -165,6 +179,12 @@ function EditIndexerModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -204,6 +224,7 @@ EditIndexerModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
@@ -23,7 +24,8 @@ const mapDispatchToProps = {
setIndexerValue,
setIndexerFieldValue,
saveIndexer,
testIndexer
testIndexer,
toggleAdvancedSettings
};
class EditIndexerModalContentConnector extends Component {
@@ -56,6 +58,10 @@ class EditIndexerModalContentConnector extends Component {
this.props.testIndexer({ id: this.props.id });
}
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
}
//
// Render
@@ -65,6 +71,7 @@ class EditIndexerModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -80,6 +87,7 @@ EditIndexerModalContentConnector.propTypes = {
item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

View File

@@ -71,7 +71,7 @@ class IndexerIndexRow extends Component {
const {
id,
name,
baseUrl,
indexerUrls,
enable,
redirect,
tags,
@@ -248,7 +248,7 @@ class IndexerIndexRow extends Component {
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={'Website'}
to={baseUrl.replace('api.', '')}
to={indexerUrls[0].replace('api.', '')}
/>
<IconButton
@@ -289,7 +289,7 @@ class IndexerIndexRow extends Component {
IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired,
baseUrl: PropTypes.string.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,

View File

@@ -31,7 +31,7 @@ function IndexerStatusCell(props) {
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon: icons.BLACKLIST}
name={enabled ? enableIcon: icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
/>
}

View File

@@ -18,7 +18,7 @@ function IndexerInfoModalContent(props) {
description,
encoding,
language,
baseUrl,
indexerUrls,
protocol,
onModalClose
} = props;
@@ -54,10 +54,10 @@ function IndexerInfoModalContent(props) {
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={baseUrl}>{baseUrl}</Link>
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url</DescriptionListItemTitle>
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
@@ -74,7 +74,7 @@ IndexerInfoModalContent.propTypes = {
description: PropTypes.string.isRequired,
encoding: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
protocol: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -10,7 +10,6 @@ function createMapStateToProps() {
(state) => state.settings.advancedSettings,
createIndexerSelector(),
(advancedSettings, indexer) => {
console.log(indexer);
return {
advancedSettings,
...indexer

View File

@@ -26,7 +26,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Protocol
{translate('Protocol')}
</SortMenuItem>
<SortMenuItem
@@ -35,7 +35,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Age
{translate('Age')}
</SortMenuItem>
<SortMenuItem
@@ -53,7 +53,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Indexer
{translate('Indexer')}
</SortMenuItem>
<SortMenuItem
@@ -62,7 +62,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Size
{translate('Size')}
</SortMenuItem>
<SortMenuItem
@@ -71,7 +71,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Files
{translate('Files')}
</SortMenuItem>
<SortMenuItem
@@ -80,7 +80,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Grabs
{translate('Grabs')}
</SortMenuItem>
<SortMenuItem
@@ -89,7 +89,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Peers
{translate('Peers')}
</SortMenuItem>
<SortMenuItem
@@ -98,7 +98,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Category
{translate('Category')}
</SortMenuItem>
</MenuContent>
</SortMenu>

View File

@@ -19,7 +19,7 @@ function NoSearchResults(props) {
return (
<div>
<div className={styles.message}>
No search results found, try performing a new search below.
{translate('NoSearchResultsFound')}
</div>
</div>
);

View File

@@ -7,6 +7,7 @@ import TextInput from 'Components/Form/TextInput';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import translate from 'Utilities/String/translate';
import SearchFooterLabel from './SearchFooterLabel';
import styles from './SearchFooter.css';
@@ -26,7 +27,7 @@ class SearchFooter extends Component {
this.state = {
searchingReleases: false,
searchQuery: defaultSearchQuery,
searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories
};
@@ -51,14 +52,12 @@ class SearchFooter extends Component {
isFetching,
defaultIndexerIds,
defaultCategories,
defaultSearchQuery,
searchError
} = this.props;
const {
searchIndexerIds,
searchCategories,
searchQuery
searchCategories
} = this.state;
const newState = {};
@@ -71,10 +70,6 @@ class SearchFooter extends Component {
newState.searchCategories = defaultCategories;
}
if (searchQuery !== defaultSearchQuery) {
newState.searchQuery = defaultSearchQuery;
}
if (prevProps.isFetching && !isFetching && !searchError) {
newState.searchingReleases = false;
}
@@ -91,6 +86,10 @@ class SearchFooter extends Component {
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories);
}
onSearchInputChange = ({ value }) => {
this.setState({ searchQuery: value });
}
//
// Render
@@ -120,7 +119,7 @@ class SearchFooter extends Component {
autoFocus={true}
value={searchQuery}
isDisabled={isFetching}
onChange={onInputChange}
onChange={this.onSearchInputChange}
/>
</div>
@@ -167,7 +166,7 @@ class SearchFooter extends Component {
isDisabled={isFetching || !hasIndexers}
onPress={this.onSearchPress}
>
Search
{translate('Search')}
</SpinnerButton>
</div>
</div>

View File

@@ -1,10 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Tooltip from '../../Components/Tooltip/Tooltip';
function CategoryLabel({ categories }) {
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
if (categories?.length === 0) {
return (
<Tooltip
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
position={tooltipPositions.LEFT}
/>
);
}
return (
<span>
{
@@ -20,6 +32,10 @@ function CategoryLabel({ categories }) {
);
}
CategoryLabel.defaultProps = {
categories: []
};
CategoryLabel.propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired
};

View File

@@ -10,7 +10,8 @@ import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress
onAdvancedSettingsPress,
showLabel
} = props;
return (
@@ -43,18 +44,27 @@ function AdvancedSettingsButton(props) {
/>
</span>
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
</div>
</div>
{
showLabel &&
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
</div>
</div>
}
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired
onAdvancedSettingsPress: PropTypes.func.isRequired,
showLabel: PropTypes.bool.isRequired
};
AdvancedSettingsButton.defaultProps = {
showLabel: true
};
export default AdvancedSettingsButton;

View File

@@ -0,0 +1,44 @@
.indexerProxy {
composes: card from '~Components/Card.css';
position: relative;
width: 300px;
height: 100px;
}
.underlay {
@add-mixin cover;
}
.overlay {
@add-mixin linkOverlay;
padding: 10px;
}
.name {
text-align: center;
font-weight: lighter;
font-size: 24px;
}
.actions {
margin-top: 20px;
text-align: right;
}
.presetsMenu {
composes: menu from '~Components/Menu/Menu.css';
display: inline-block;
margin: 0 5px;
}
.presetsMenuButton {
composes: button from '~Components/Link/Button.css';
&::after {
margin-left: 5px;
content: '\25BE';
}
}

View File

@@ -0,0 +1,111 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerProxyPresetMenuItem from './AddIndexerProxyPresetMenuItem';
import styles from './AddIndexerProxyItem.css';
class AddIndexerProxyItem extends Component {
//
// Listeners
onIndexerProxySelect = () => {
const {
implementation
} = this.props;
this.props.onIndexerProxySelect({ implementation });
}
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onIndexerProxySelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.indexerProxy}
>
<Link
className={styles.underlay}
onPress={this.onIndexerProxySelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onIndexerProxySelect}
>
Custom
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
Presets
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerProxyPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
onPress={onIndexerProxySelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddIndexerProxyItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onIndexerProxySelect: PropTypes.func.isRequired
};
export default AddIndexerProxyItem;

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerProxyModalContentConnector from './AddIndexerProxyModalContentConnector';
function AddIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddIndexerProxyModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddIndexerProxyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerProxyModal;

View File

@@ -0,0 +1,5 @@
.indexerProxies {
display: flex;
justify-content: center;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
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 translate from 'Utilities/String/translate';
import AddIndexerProxyItem from './AddIndexerProxyItem';
import styles from './AddIndexerProxyModalContent.css';
class AddIndexerProxyModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema,
onIndexerProxySelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddIndexerProxy')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<div>
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
</div>
}
{
isSchemaPopulated && !schemaError &&
<div>
<div className={styles.indexerProxies}>
{
schema.map((indexerProxy) => {
return (
<AddIndexerProxyItem
key={indexerProxy.implementation}
implementation={indexerProxy.implementation}
{...indexerProxy}
onIndexerProxySelect={onIndexerProxySelect}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerProxyModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerProxySelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerProxyModalContent;

View File

@@ -0,0 +1,70 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerProxySchema, selectIndexerProxySchema } from 'Store/Actions/settingsActions';
import AddIndexerProxyModalContent from './AddIndexerProxyModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexerProxies,
(indexerProxies) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = indexerProxies;
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
};
}
);
}
const mapDispatchToProps = {
fetchIndexerProxySchema,
selectIndexerProxySchema
};
class AddIndexerProxyModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerProxySchema();
}
//
// Listeners
onIndexerProxySelect = ({ implementation, name }) => {
this.props.selectIndexerProxySchema({ implementation, presetName: name });
this.props.onModalClose({ indexerProxySelected: true });
}
//
// Render
render() {
return (
<AddIndexerProxyModalContent
{...this.props}
onIndexerProxySelect={this.onIndexerProxySelect}
/>
);
}
}
AddIndexerProxyModalContentConnector.propTypes = {
fetchIndexerProxySchema: PropTypes.func.isRequired,
selectIndexerProxySchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerProxyModalContentConnector);

View File

@@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerProxyPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation
} = this.props;
this.props.onPress({
name,
implementation
});
}
//
// Render
render() {
const {
name,
implementation,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerProxyPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerProxyPresetMenuItem;

View File

@@ -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 EditIndexerProxyModalContentConnector from './EditIndexerProxyModalContentConnector';
function EditIndexerProxyModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerProxyModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerProxyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerProxyModal;

View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveIndexerProxy, cancelTestIndexerProxy } from 'Store/Actions/settingsActions';
import EditIndexerProxyModal from './EditIndexerProxyModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.indexerProxies';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestIndexerProxy() {
dispatch(cancelTestIndexerProxy({ section }));
},
dispatchCancelSaveIndexerProxy() {
dispatch(cancelSaveIndexerProxy({ section }));
}
};
}
class EditIndexerProxyModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestIndexerProxy();
this.props.dispatchCancelSaveIndexerProxy();
this.props.onModalClose();
}
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestIndexerProxy,
dispatchCancelSaveIndexerProxy,
...otherProps
} = this.props;
return (
<EditIndexerProxyModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditIndexerProxyModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestIndexerProxy: PropTypes.func.isRequired,
dispatchCancelSaveIndexerProxy: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditIndexerProxyModalConnector);

View File

@@ -0,0 +1,11 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}

View File

@@ -0,0 +1,175 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
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 translate from 'Utilities/String/translate';
import styles from './EditIndexerProxyModalContent.css';
function EditIndexerProxyModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerProxyPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
tags,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToAddANewIndexerProxyPleaseTryAgain')}
</div>
}
{
!isFetching && !error &&
<Form {...otherProps}>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('TagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexerProxy"
providerData={item}
section="settings.indexerProxies"
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerProxyPress}
>
{translate('Delete')}
</Button>
}
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerProxyModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteIndexerProxyPress: PropTypes.func
};
export default EditIndexerProxyModalContent;

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexerProxy, setIndexerProxyFieldValue, setIndexerProxyValue, testIndexerProxy } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditIndexerProxyModalContent from './EditIndexerProxyModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('indexerProxies'),
(advancedSettings, indexerProxy) => {
return {
advancedSettings,
...indexerProxy
};
}
);
}
const mapDispatchToProps = {
setIndexerProxyValue,
setIndexerProxyFieldValue,
saveIndexerProxy,
testIndexerProxy
};
class EditIndexerProxyModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerProxyValue({ name, value });
}
onFieldChange = ({ name, value }) => {
this.props.setIndexerProxyFieldValue({ name, value });
}
onSavePress = () => {
this.props.saveIndexerProxy({ id: this.props.id });
}
onTestPress = () => {
this.props.testIndexerProxy({ id: this.props.id });
}
//
// Render
render() {
return (
<EditIndexerProxyModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerProxyModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerProxyValue: PropTypes.func.isRequired,
setIndexerProxyFieldValue: PropTypes.func.isRequired,
saveIndexerProxy: PropTypes.func.isRequired,
testIndexerProxy: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerProxyModalContentConnector);

View File

@@ -0,0 +1,20 @@
.indexerProxies {
display: flex;
flex-wrap: wrap;
}
.addIndexerProxy {
composes: indexerProxy from '~./IndexerProxy.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}

View File

@@ -0,0 +1,121 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerProxyModal from './AddIndexerProxyModal';
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
import IndexerProxy from './IndexerProxy';
import styles from './IndexerProxies.css';
class IndexerProxies extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddIndexerProxyModalOpen: false,
isEditIndexerProxyModalOpen: false
};
}
//
// Listeners
onAddIndexerProxyPress = () => {
this.setState({ isAddIndexerProxyModalOpen: true });
}
onAddIndexerProxyModalClose = ({ indexerProxySelected = false } = {}) => {
this.setState({
isAddIndexerProxyModalOpen: false,
isEditIndexerProxyModalOpen: indexerProxySelected
});
}
onEditIndexerProxyModalClose = () => {
this.setState({ isEditIndexerProxyModalOpen: false });
}
//
// Render
render() {
const {
items,
tagList,
indexerList,
onConfirmDeleteIndexerProxy,
...otherProps
} = this.props;
const {
isAddIndexerProxyModalOpen,
isEditIndexerProxyModalOpen
} = this.state;
return (
<FieldSet legend={translate('Indexer Proxies')}>
<PageSectionContent
errorMessage={translate('UnableToLoadIndexerProxies')}
{...otherProps}
>
<div className={styles.indexerProxies}>
{
items.map((item) => {
return (
<IndexerProxy
key={item.id}
{...item}
tagList={tagList}
indexerList={indexerList}
onConfirmDeleteIndexerProxy={onConfirmDeleteIndexerProxy}
/>
);
})
}
<Card
className={styles.addIndexerProxy}
onPress={this.onAddIndexerProxyPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddIndexerProxyModal
isOpen={isAddIndexerProxyModalOpen}
onModalClose={this.onAddIndexerProxyModalClose}
/>
<EditIndexerProxyModalConnector
isOpen={isEditIndexerProxyModalOpen}
onModalClose={this.onEditIndexerProxyModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
IndexerProxies.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
};
export default IndexerProxies;

View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName';
import IndexerProxies from './IndexerProxies';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexerProxies', sortByName),
createSortedSectionSelector('indexers', sortByName),
createTagsSelector(),
(indexerProxies, indexers, tagList) => {
return {
...indexerProxies,
indexerList: indexers.items,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchIndexerProxies,
deleteIndexerProxy
};
class IndexerProxiesConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerProxies();
}
//
// Listeners
onConfirmDeleteIndexerProxy = (id) => {
this.props.deleteIndexerProxy({ id });
}
//
// Render
render() {
return (
<IndexerProxies
{...this.props}
onConfirmDeleteIndexerProxy={this.onConfirmDeleteIndexerProxy}
/>
);
}
}
IndexerProxiesConnector.propTypes = {
fetchIndexerProxies: PropTypes.func.isRequired,
deleteIndexerProxy: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerProxiesConnector);

View File

@@ -0,0 +1,23 @@
.indexerProxy {
composes: card from '~Components/Card.css';
width: 290px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.indexers {
flex: 1 0 auto;
}
.enabled {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}

View File

@@ -0,0 +1,144 @@
import _ from 'lodash';
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 TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditIndexerProxyModalConnector from './EditIndexerProxyModalConnector';
import styles from './IndexerProxy.css';
class IndexerProxy extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerProxyModalOpen: false,
isDeleteIndexerProxyModalOpen: false
};
}
//
// Listeners
onEditIndexerProxyPress = () => {
this.setState({ isEditIndexerProxyModalOpen: true });
}
onEditIndexerProxyModalClose = () => {
this.setState({ isEditIndexerProxyModalOpen: false });
}
onDeleteIndexerProxyPress = () => {
this.setState({
isEditIndexerProxyModalOpen: false,
isDeleteIndexerProxyModalOpen: true
});
}
onDeleteIndexerProxyModalClose= () => {
this.setState({ isDeleteIndexerProxyModalOpen: false });
}
onConfirmDeleteIndexerProxy = () => {
this.props.onConfirmDeleteIndexerProxy(this.props.id);
}
//
// Render
render() {
const {
id,
name,
tags,
tagList,
indexerList
} = this.props;
return (
<Card
className={styles.indexerProxy}
overlayContent={true}
onPress={this.onEditIndexerProxyPress}
>
<div className={styles.name}>
{name}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div className={styles.indexers}>
{
tags.map((t) => {
const indexers = _.filter(indexerList, { tags: [t] });
if (!indexers || indexers.length === 0) {
return null;
}
return indexers.map((i) => {
return (
<Label
key={i.name}
kind={kinds.SUCCESS}
>
{i.name}
</Label>
);
});
})
}
</div>
{
!tags || tags.length === 0 ?
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label> :
null
}
<EditIndexerProxyModalConnector
id={id}
isOpen={this.state.isEditIndexerProxyModalOpen}
onModalClose={this.onEditIndexerProxyModalClose}
onDeleteIndexerProxyPress={this.onDeleteIndexerProxyPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerProxyModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexerProxy')}
message={translate('DeleteIndexerProxyMessageText', [name])}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexerProxy}
onCancel={this.onDeleteIndexerProxyModalClose}
/>
</Card>
);
}
}
IndexerProxy.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteIndexerProxy: PropTypes.func.isRequired
};
export default IndexerProxy;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import IndexerProxiesConnector from './IndexerProxies/IndexerProxiesConnector';
function IndexerSettings() {
return (
<PageContent title={translate('Proxies')}>
<SettingsToolbarConnector
showSave={false}
/>
<PageContentBody>
<IndexerProxiesConnector />
</PageContentBody>
</PageContent>
);
}
export default IndexerSettings;

View File

@@ -16,13 +16,24 @@ function Settings() {
<PageContentBody>
<Link
className={styles.link}
to="/settings/applications"
to="/settings/indexers"
>
Applications
{translate('Indexers')}
</Link>
<div className={styles.summary}>
Applications and settings to configure how prowlarr interacts with your PVR programs
{translate('IndexerSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/applications"
>
{translate('Apps')}
</Link>
<div className={styles.summary}>
{translate('AppSettingsSummary')}
</div>
<Link
@@ -40,7 +51,7 @@ function Settings() {
className={styles.link}
to="/settings/connect"
>
Notifications
{translate('Notifications')}
</Link>
<div className={styles.summary}>

View File

@@ -1,47 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import titleCase from 'Utilities/String/titleCase';
function TagDetailsDelayProfile(props) {
const {
preferredProtocol,
enableUsenet,
enableTorrent,
usenetDelay,
torrentDelay
} = props;
return (
<div>
<div>
Protocol: {titleCase(preferredProtocol)}
</div>
<div>
{
enableUsenet ?
`Usenet Delay: ${usenetDelay}` :
'Usenet disabled'
}
</div>
<div>
{
enableTorrent ?
`Torrent Delay: ${torrentDelay}` :
'Torrents disabled'
}
</div>
</div>
);
}
TagDetailsDelayProfile.propTypes = {
preferredProtocol: PropTypes.string.isRequired,
enableUsenet: PropTypes.bool.isRequired,
enableTorrent: PropTypes.bool.isRequired,
usenetDelay: PropTypes.number.isRequired,
torrentDelay: PropTypes.number.isRequired
};
export default TagDetailsDelayProfile;

View File

@@ -1,26 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
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 { kinds } from 'Helpers/Props';
import split from 'Utilities/String/split';
import translate from 'Utilities/String/translate';
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
import styles from './TagDetailsModalContent.css';
function TagDetailsModalContent(props) {
const {
label,
isTagUsed,
movies,
delayProfiles,
indexers,
notifications,
restrictions,
indexerProxies,
onModalClose,
onDeleteTagPress
} = props;
@@ -40,13 +36,13 @@ function TagDetailsModalContent(props) {
}
{
!!movies.length &&
<FieldSet legend={translate('Movies')}>
!!indexers.length &&
<FieldSet legend={translate('Indexers')}>
{
movies.map((item) => {
indexers.map((item) => {
return (
<div key={item.id}>
{item.title}
{item.name}
</div>
);
})
@@ -54,35 +50,6 @@ function TagDetailsModalContent(props) {
</FieldSet>
}
{
!!delayProfiles.length &&
<FieldSet legend={translate('DelayProfile')}>
{
delayProfiles.map((item) => {
const {
id,
preferredProtocol,
enableUsenet,
enableTorrent,
usenetDelay,
torrentDelay
} = item;
return (
<TagDetailsDelayProfile
key={id}
preferredProtocol={preferredProtocol}
enableUsenet={enableUsenet}
enableTorrent={enableTorrent}
usenetDelay={usenetDelay}
torrentDelay={torrentDelay}
/>
);
})
}
</FieldSet>
}
{
!!notifications.length &&
<FieldSet legend={translate('Connections')}>
@@ -99,44 +66,13 @@ function TagDetailsModalContent(props) {
}
{
!!restrictions.length &&
<FieldSet legend={translate('Restrictions')}>
!!indexerProxies.length &&
<FieldSet legend={translate('Indexer Proxies')}>
{
restrictions.map((item) => {
indexerProxies.map((item) => {
return (
<div
key={item.id}
className={styles.restriction}
>
<div>
{
split(item.required).map((r) => {
return (
<Label
key={r}
kind={kinds.SUCCESS}
>
{r}
</Label>
);
})
}
</div>
<div>
{
split(item.ignored).map((i) => {
return (
<Label
key={i}
kind={kinds.DANGER}
>
{i}
</Label>
);
})
}
</div>
<div key={item.id}>
{item.name}
</div>
);
})
@@ -171,10 +107,9 @@ function TagDetailsModalContent(props) {
TagDetailsModalContent.propTypes = {
label: PropTypes.string.isRequired,
isTagUsed: PropTypes.bool.isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
restrictions: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import TagDetailsModalContent from './TagDetailsModalContent';
function findMatchingItems(ids, items) {
@@ -9,35 +8,15 @@ function findMatchingItems(ids, items) {
});
}
function createUnorderedMatchingMoviesSelector() {
function createMatchingIndexersSelector() {
return createSelector(
(state, { indexerIds }) => indexerIds,
createAllIndexersSelector(),
(state) => state.indexers.items,
findMatchingItems
);
}
function createMatchingMoviesSelector() {
return createSelector(
createUnorderedMatchingMoviesSelector(),
(movies) => {
return movies.sort((movieA, movieB) => {
const sortTitleA = movieA.sortTitle;
const sortTitleB = movieB.sortTitle;
if (sortTitleA > sortTitleB) {
return 1;
} else if (sortTitleA < sortTitleB) {
return -1;
}
return 0;
});
}
);
}
function createMatchingNotificationsSelector() {
function createMatchingIndexerProxiesSelector() {
return createSelector(
(state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items,
@@ -45,13 +24,23 @@ function createMatchingNotificationsSelector() {
);
}
function createMatchingNotificationsSelector() {
return createSelector(
(state, { indexerProxyIds }) => indexerProxyIds,
(state) => state.settings.indexerProxies.items,
findMatchingItems
);
}
function createMapStateToProps() {
return createSelector(
createMatchingMoviesSelector(),
createMatchingIndexersSelector(),
createMatchingIndexerProxiesSelector(),
createMatchingNotificationsSelector(),
(movies, notifications) => {
(indexers, indexerProxies, notifications) => {
return {
movies,
indexers,
indexerProxies,
notifications
};
}

View File

@@ -53,11 +53,9 @@ class Tag extends Component {
render() {
const {
label,
delayProfileIds,
notificationIds,
restrictionIds,
importListIds,
movieIds
indexerIds,
indexerProxyIds
} = this.props;
const {
@@ -66,11 +64,9 @@ class Tag extends Component {
} = this.state;
const isTagUsed = !!(
delayProfileIds.length ||
indexerIds.length ||
notificationIds.length ||
restrictionIds.length ||
importListIds.length ||
movieIds.length
indexerProxyIds.length
);
return (
@@ -87,16 +83,9 @@ class Tag extends Component {
isTagUsed &&
<div>
{
!!movieIds.length &&
!!indexerIds.length &&
<div>
{movieIds.length} movies
</div>
}
{
!!delayProfileIds.length &&
<div>
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
</div>
}
@@ -108,16 +97,9 @@ class Tag extends Component {
}
{
!!restrictionIds.length &&
!!indexerProxyIds.length &&
<div>
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
</div>
}
{
!!importListIds.length &&
<div>
{importListIds.length} list{importListIds.length > 1 && 's'}
{indexerProxyIds.length} indexerProxy{indexerProxyIds.length > 1 && 's'}
</div>
}
</div>
@@ -133,11 +115,9 @@ class Tag extends Component {
<TagDetailsModal
label={label}
isTagUsed={isTagUsed}
movieIds={movieIds}
delayProfileIds={delayProfileIds}
indexerIds={indexerIds}
notificationIds={notificationIds}
restrictionIds={restrictionIds}
importListIds={importListIds}
indexerProxyIds={indexerProxyIds}
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress}
@@ -160,20 +140,16 @@ class Tag extends Component {
Tag.propTypes = {
id: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired
};
Tag.defaultProps = {
delayProfileIds: [],
indexerIds: [],
notificationIds: [],
restrictionIds: [],
importListIds: [],
movieIds: []
indexerProxyIds: []
};
export default Tag;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags';
@@ -26,7 +26,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchNotifications: fetchNotifications
dispatchFetchNotifications: fetchNotifications,
dispatchFetchIndexerProxies: fetchIndexerProxies
};
class MetadatasConnector extends Component {
@@ -37,11 +38,13 @@ class MetadatasConnector extends Component {
componentDidMount() {
const {
dispatchFetchTagDetails,
dispatchFetchNotifications
dispatchFetchNotifications,
dispatchFetchIndexerProxies
} = this.props;
dispatchFetchTagDetails();
dispatchFetchNotifications();
dispatchFetchIndexerProxies();
}
//
@@ -58,7 +61,8 @@ class MetadatasConnector extends Component {
MetadatasConnector.propTypes = {
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired
dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchIndexerProxies: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);

View File

@@ -10,7 +10,7 @@ import {
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
const blacklistedProperties = [
const omittedProperties = [
'section',
'id'
];
@@ -31,7 +31,7 @@ export default function createHandleActions(handlers, defaultState, section) {
if (section === baseSection) {
const newState = Object.assign(getSectionState(state, payloadSection),
_.omit(payload, blacklistedProperties));
_.omit(payload, omittedProperties));
return updateSectionState(state, payloadSection, newState);
}

View File

@@ -0,0 +1,110 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
//
// Variables
const section = 'settings.indexerProxies';
//
// Actions Types
export const FETCH_INDEXER_PROXYS = 'settings/indexerProxies/fetchIndexerProxies';
export const FETCH_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/fetchIndexerProxySchema';
export const SELECT_INDEXER_PROXY_SCHEMA = 'settings/indexerProxies/selectIndexerProxySchema';
export const SET_INDEXER_PROXY_VALUE = 'settings/indexerProxies/setIndexerProxyValue';
export const SET_INDEXER_PROXY_FIELD_VALUE = 'settings/indexerProxies/setIndexerProxyFieldValue';
export const SAVE_INDEXER_PROXY = 'settings/indexerProxies/saveIndexerProxy';
export const CANCEL_SAVE_INDEXER_PROXY = 'settings/indexerProxies/cancelSaveIndexerProxy';
export const DELETE_INDEXER_PROXY = 'settings/indexerProxies/deleteIndexerProxy';
export const TEST_INDEXER_PROXY = 'settings/indexerProxies/testIndexerProxy';
export const CANCEL_TEST_INDEXER_PROXY = 'settings/indexerProxies/cancelTestIndexerProxy';
//
// Action Creators
export const fetchIndexerProxies = createThunk(FETCH_INDEXER_PROXYS);
export const fetchIndexerProxySchema = createThunk(FETCH_INDEXER_PROXY_SCHEMA);
export const selectIndexerProxySchema = createAction(SELECT_INDEXER_PROXY_SCHEMA);
export const saveIndexerProxy = createThunk(SAVE_INDEXER_PROXY);
export const cancelSaveIndexerProxy = createThunk(CANCEL_SAVE_INDEXER_PROXY);
export const deleteIndexerProxy = createThunk(DELETE_INDEXER_PROXY);
export const testIndexerProxy = createThunk(TEST_INDEXER_PROXY);
export const cancelTestIndexerProxy = createThunk(CANCEL_TEST_INDEXER_PROXY);
export const setIndexerProxyValue = createAction(SET_INDEXER_PROXY_VALUE, (payload) => {
return {
section,
...payload
};
});
export const setIndexerProxyFieldValue = createAction(SET_INDEXER_PROXY_FIELD_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: [],
selectedSchema: {},
isSaving: false,
saveError: null,
isTesting: false,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_PROXYS]: createFetchHandler(section, '/indexerProxy'),
[FETCH_INDEXER_PROXY_SCHEMA]: createFetchSchemaHandler(section, '/indexerProxy/schema'),
[SAVE_INDEXER_PROXY]: createSaveProviderHandler(section, '/indexerProxy'),
[CANCEL_SAVE_INDEXER_PROXY]: createCancelSaveProviderHandler(section),
[DELETE_INDEXER_PROXY]: createRemoveItemHandler(section, '/indexerProxy'),
[TEST_INDEXER_PROXY]: createTestProviderHandler(section, '/indexerProxy'),
[CANCEL_TEST_INDEXER_PROXY]: createCancelTestProviderHandler(section)
},
//
// Reducers
reducers: {
[SET_INDEXER_PROXY_VALUE]: createSetSettingValueReducer(section),
[SET_INDEXER_PROXY_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
return selectedSchema;
});
}
}
};

View File

@@ -202,7 +202,8 @@ export const defaultState = {
export const persistState = [
'releases.customFilters',
'releases.selectedFilterKey'
'releases.selectedFilterKey',
'releases.columns'
];
//

View File

@@ -7,6 +7,7 @@ import development from './Settings/development';
import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
@@ -14,6 +15,7 @@ import ui from './Settings/ui';
export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies';
export * from './Settings/languages';
export * from './Settings/notifications';
export * from './Settings/applications';
@@ -35,6 +37,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState,
notifications: notifications.defaultState,
applications: applications.defaultState,
@@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...general.actionHandlers,
...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers,
...languages.actionHandlers,
...notifications.actionHandlers,
...applications.actionHandlers,
@@ -84,6 +88,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...general.reducers,
...indexerCategories.reducers,
...indexerProxies.reducers,
...languages.reducers,
...notifications.reducers,
...applications.reducers,

View File

@@ -43,6 +43,7 @@ function getInternalLink(source) {
function getTestLink(source, props) {
switch (source) {
case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return (
<SpinnerIconButton
name={icons.TEST}

View File

@@ -30,9 +30,9 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "5.0.6",
"@sentry/browser": "6.3.1",
"@sentry/integrations": "6.3.1",
"@microsoft/signalr": "5.0.9",
"@sentry/browser": "6.10.0",
"@sentry/integrations": "6.10.0",
"chart.js": "3.2.0",
"classnames": "2.3.1",
"clipboard": "2.0.8",
@@ -98,12 +98,12 @@
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.11.0",
"css-loader": "5.2.4",
"eslint": "7.25.0",
"eslint": "7.31.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-react": "7.23.2",
"eslint-plugin-import": "2.23.4",
"eslint-plugin-react": "7.24.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "2.0.0",
"esprint": "3.1.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "5.0.0",
"html-webpack-plugin": "5.3.1",
@@ -125,7 +125,7 @@
"webpack": "5.35.1",
"webpack-cli": "4.6.0",
"webpack-livereload-plugin": "3.0.1",
"stylelint": "13.13.0",
"stylelint": "13.13.1",
"stylelint-order": "4.1.0"
}
}

View File

@@ -94,7 +94,7 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />

View File

@@ -20,6 +20,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://horrorcharnel.org/takeloginhorror.php: username=mySecret&password=mySecret&use_sslvalue==&perm_ssl=1&submitme=X&use_ssl=1&returnto=%2F&captchaSelection=1230456")]
[TestCase(@"https://torrentdb.net/login: _token=2b51db35e1912ffc138825a12b9933d2&username=mySecret&password=mySecret&remember=on")]
[TestCase(@" var authkey = ""2b51db35e1910123321025a12b9933d2"";")]
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
@@ -100,5 +102,13 @@ namespace NzbDrone.Common.Test.InstrumentationTests
cleansedMessage.Should().Be(message);
}
[TestCase(@"https://www.torrentleech.org/torrents/browse/list/imdbID/tt8005374/categories/29,2,26,27,32,44,7,34,35")]
public void should_not_clean_url(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().Be(message);
}
}
}

View File

@@ -53,6 +53,16 @@ namespace NzbDrone.Common.Extensions
return dateTime >= afterDateTime && dateTime <= beforeDateTime;
}
public static DateTime EndOfDay(this DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
}
public static DateTime StartOfDay(this DateTime date)
{
return new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0);
}
public static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
}
}

View File

@@ -194,5 +194,21 @@ namespace NzbDrone.Common.Extensions
var inputBytes = encoding.GetBytes(searchString);
return encoding.GetString(WebUtility.UrlDecodeToBytes(inputBytes, 0, inputBytes.Length));
}
public static string CleanFileName(this string name)
{
string result = name;
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
result = result.Replace(": ", " - ");
for (int i = 0; i < badCharacters.Length; i++)
{
result = result.Replace(badCharacters[i], goodCharacters[i]);
}
return result.TrimStart(' ', '.').TrimEnd(' ');
}
}
}

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
webRequest.Proxy = GetProxy(request.Url);
webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
if (request.Headers != null)
{

View File

@@ -61,7 +61,7 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
}
public async Task<HttpResponse> ExecuteAsync(HttpRequest request)
public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
{
var cookieContainer = InitializeRequestCookies(request);

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -30,6 +31,8 @@ namespace NzbDrone.Common.Http
public HttpUri Url { get; set; }
public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; }
@@ -75,8 +78,28 @@ namespace NzbDrone.Common.Http
public void SetContent(string data)
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
if (Encoding != null)
{
ContentData = Encoding.GetBytes(data);
}
else
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
}
}
public string GetContent()
{
if (Encoding != null)
{
return Encoding.GetString(ContentData);
}
else
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
return encoding.GetString(ContentData);
}
}
public void AddBasicAuthentication(string username, string password)

View File

@@ -47,7 +47,14 @@ namespace NzbDrone.Common.Http
{
if (_content == null)
{
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
if (Request.Encoding != null)
{
_content = Request.Encoding.GetString(ResponseData);
}
else
{
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
}
}
return _content;

View File

@@ -11,14 +11,15 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd|pwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&| )[^=]*?(_?(?<!use)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -5,12 +5,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="DryIoc.dll" Version="4.7.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="Sentry" Version="3.3.3" />
<PackageReference Include="Sentry" Version="3.8.3" />
<PackageReference Include="SharpZipLib" Version="1.3.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />

View File

@@ -33,9 +33,9 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;

View File

@@ -32,9 +32,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
{
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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 { Categories = new int[] { 2000 } })).Releases;

View File

@@ -19,7 +19,8 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Subject.Settings = new FileListSettings()
{
Passkey = "abcd",
Username = "somename"
Username = "somename",
BaseUrl = "https://filelist.io"
};
Subject.Capabilities = new IndexerCapabilities
@@ -54,8 +55,6 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
SearchTerm = "Star Wars",
Categories = new int[] { 2000 }
};
Subject.BaseUrl = "https://filelist.io";
}
private void MovieWithoutIMDB()

View File

@@ -44,9 +44,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
{
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
@@ -73,9 +73,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
{
var responseJson = new { status = 5, message = "Invalid authentication credentials" }.ToJson();
Mocker.GetMock<IHttpClient>()
.Setup(v => v.ExecuteAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.IsAny<HttpRequest>(), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson))));
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;

View File

@@ -1,10 +1,12 @@
using System;
using System.Net;
using System.Threading.Tasks;
using System.Xml;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework;
@@ -14,6 +16,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
{
private NewznabSettings _settings;
private IndexerDefinition _definition;
private string _caps;
[SetUp]
@@ -24,14 +27,24 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
BaseUrl = "http://indxer.local"
};
_definition = new IndexerDefinition()
{
Id = 5,
Name = "Newznab",
Settings = new NewznabSettings()
{
BaseUrl = "http://indexer.local/"
}
};
_caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml");
}
private void GivenCapsResponse(string caps)
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Returns<HttpRequest, IndexerDefinition>((r, d) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
}
[Test]
@@ -39,11 +52,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps);
Subject.GetCapabilities(_settings);
Subject.GetCapabilities(_settings);
Subject.GetCapabilities(_settings, _definition);
Subject.GetCapabilities(_settings, _definition);
Mocker.GetMock<IHttpClient>()
.Verify(o => o.Get(It.IsAny<HttpRequest>()), Times.Once());
Mocker.GetMock<IIndexerHttpClient>()
.Verify(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()), Times.Once());
}
[Test]
@@ -51,7 +64,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings);
var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(25);
caps.LimitsMax.Value.Should().Be(60);
@@ -62,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("<limits", "<abclimits"));
var caps = Subject.GetCapabilities(_settings);
var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(100);
caps.LimitsMax.Value.Should().Be(100);
@@ -71,11 +84,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test]
public void should_throw_if_failed_to_get()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Throws<Exception>();
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings));
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings, _definition));
}
[Test]
@@ -83,7 +96,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("<limits", "<>"));
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings));
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings, _definition));
}
[Test]
@@ -91,7 +104,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
GivenCapsResponse(_caps.Replace("5030", "asdf"));
var result = Subject.GetCapabilities(_settings);
var result = Subject.GetCapabilities(_settings, _definition);
result.Should().NotBeNull();
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}
@@ -42,9 +42,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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 { Categories = new int[] { 2000 }, Limit = 100, Offset = 0 })).Releases;

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
{
private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchCriteria;
private IndexerCapabilities _capabilities;
[SetUp]
@@ -30,10 +31,17 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
Categories = new int[] { 2000 }
};
_tvSearchCriteria = new TvSearchCriteria
{
SearchTerm = "Star Wars",
Categories = new int[] { 5000 },
Season = 0
};
_capabilities = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_capabilities);
}
@@ -178,5 +186,18 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
pageTier2.Url.Query.Should().NotContain("imdbid=0076759");
pageTier2.Url.Query.Should().Contain("q=");
}
[Test]
public void should_pad_seasons_for_tv_search()
{
_capabilities.TvSearchParams = new List<TvSearchParam> { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep };
var results = Subject.GetSearchRequests(_tvSearchCriteria);
results.Tiers.Should().Be(1);
var pageTier = results.GetTier(0).First().First();
pageTier.Url.Query.Should().Contain("season=00");
}
}
}

View File

@@ -36,13 +36,13 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
Json.Serialize(authResponse, authStream);
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson)));
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;

View File

@@ -37,9 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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 { Categories = new int[] { 2000 } })).Releases;
@@ -64,9 +64,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test]
public async Task should_parse_error_20_as_empty_results()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }")));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -76,9 +76,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test]
public async Task should_warn_on_unknown_error()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }")));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;

View File

@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Test.IndexerTests
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings>
{
public override string Name => "Test Indexer";
public override string BaseUrl => "http://testindexer.com";
public override string[] IndexerUrls => new string[] { "http://testindexer.com" };
public override string Description => "";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
@@ -19,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public int _supportedPageSize;
public override int PageSize => _supportedPageSize;
public TestIndexer(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public TestIndexer(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
}

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Test.IndexerTests
{
public class TestIndexerSettings : IProviderConfig
public class TestIndexerSettings : IIndexerSettings
{
public NzbDroneValidationResult Validate()
{
@@ -14,5 +12,6 @@ namespace NzbDrone.Core.Test.IndexerTests
}
public string BaseUrl { get; set; }
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
}
}

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}
@@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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;
@@ -72,9 +72,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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;
@@ -102,9 +102,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(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;

View File

@@ -3,10 +3,10 @@
<TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="YamlDotNet" Version="11.1.1" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -7,5 +7,6 @@ namespace NzbDrone.Core.Applications
public int IndexerId { get; set; }
public int AppId { get; set; }
public int RemoteIndexerId { get; set; }
public string RemoteIndexerName { get; set; }
}
}

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Applications
{
List<AppIndexerMap> GetMappingsForApp(int appId);
AppIndexerMap Insert(AppIndexerMap appIndexerMap);
AppIndexerMap Update(AppIndexerMap appIndexerMap);
void Delete(int mappingId);
void DeleteAllForApp(int appId);
}
@@ -41,6 +42,11 @@ namespace NzbDrone.Core.Applications
return _appIndexerMapRepository.Insert(appIndexerMap);
}
public AppIndexerMap Update(AppIndexerMap appIndexerMap)
{
return _appIndexerMapRepository.Update(appIndexerMap);
}
public void Handle(ProviderDeletedEvent<IApplication> message)
{
_appIndexerMapRepository.DeleteAllForApp(message.ProviderId);

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Applications
protected readonly IAppIndexerMapService _appIndexerMapService;
protected readonly Logger _logger;
protected static readonly Regex AppIndexerRegex = new Regex(@"(?<indexer>\d*)/api",
protected static readonly Regex AppIndexerRegex = new Regex(@"\/(?<indexer>\d{1,3})(?:\/(?:api)?\/?)?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public abstract string Name { get; }
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Applications
public abstract void AddIndexer(IndexerDefinition indexer);
public abstract void UpdateIndexer(IndexerDefinition indexer);
public abstract void RemoveIndexer(int indexerId);
public abstract Dictionary<int, int> GetIndexerMappings();
public abstract List<AppIndexerMap> GetIndexerMappings();
public virtual object RequestAction(string action, IDictionary<string, string> query)
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Indexers;
@@ -110,18 +111,27 @@ namespace NzbDrone.Core.Applications
{
var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id);
//Remote-Local mappings currently stored by Prowlarr
var prowlarrMappings = indexerMappings.ToDictionary(i => i.RemoteIndexerId, i => i.IndexerId);
//Get Dictionary of Remote Indexers point to Prowlarr and what they are mapped to
var remoteMappings = app.GetIndexerMappings();
var remoteMappings = ExecuteAction(a => a.GetIndexerMappings(), app);
if (remoteMappings == null)
{
continue;
}
//Add mappings if not already in db, these were setup manually in the app or orphaned by a table wipe
foreach (var mapping in remoteMappings)
{
if (!prowlarrMappings.ContainsKey(mapping.Key))
if (!indexerMappings.Any(m => (m.RemoteIndexerId > 0 && m.RemoteIndexerId == mapping.RemoteIndexerId) || (m.RemoteIndexerName.IsNotNullOrWhiteSpace() && m.RemoteIndexerName == mapping.RemoteIndexerName)))
{
var addMapping = new AppIndexerMap { AppId = app.Definition.Id, RemoteIndexerId = mapping.Key, IndexerId = mapping.Value };
var addMapping = new AppIndexerMap
{
AppId = app.Definition.Id,
RemoteIndexerId = mapping.RemoteIndexerId,
RemoteIndexerName = mapping.RemoteIndexerName,
IndexerId = mapping.IndexerId
};
_appIndexerMapService.Insert(addMapping);
indexerMappings.Add(addMapping);
}
@@ -214,5 +224,64 @@ namespace NzbDrone.Core.Applications
_logger.Error(ex, "An error occurred while talking to remote application.");
}
}
private TResult ExecuteAction<TResult>(Func<IApplication, TResult> applicationAction, IApplication application)
{
TResult result;
try
{
result = applicationAction(application);
_applicationStatusService.RecordSuccess(application.Definition.Id);
return result;
}
catch (WebException webException)
{
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
webException.Status == WebExceptionStatus.ConnectFailure)
{
_applicationStatusService.RecordConnectionFailure(application.Definition.Id);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id);
}
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
}
else
{
_logger.Warn("{0} {1}", this, webException.Message);
}
}
catch (TooManyRequestsException ex)
{
if (ex.RetryAfter != TimeSpan.Zero)
{
_applicationStatusService.RecordFailure(application.Definition.Id, ex.RetryAfter);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id, TimeSpan.FromHours(1));
}
_logger.Warn("API Request Limit reached for {0}", this);
}
catch (HttpException ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Warn("{0} {1}", this, ex.Message);
}
catch (Exception ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Error(ex, "An error occurred while talking to remote application.");
}
return default(TResult);
}
}
}

View File

@@ -9,6 +9,6 @@ namespace NzbDrone.Core.Applications
void AddIndexer(IndexerDefinition indexer);
void UpdateIndexer(IndexerDefinition indexer);
void RemoveIndexer(int indexerId);
Dictionary<int, int> GetIndexerMappings();
List<AppIndexerMap> GetIndexerMappings();
}
}

View File

@@ -55,12 +55,12 @@ namespace NzbDrone.Core.Applications.Lidarr
return new ValidationResult(failures);
}
public override Dictionary<int, int> GetIndexerMappings()
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _lidarrV1Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new Dictionary<int, int>();
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(indexer.Id, indexerId);
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Mylar
{
public class Mylar : ApplicationBase<MylarSettings>
{
public override string Name => "Mylar";
private readonly IMylarV3Proxy _mylarV3Proxy;
private readonly IConfigFileProvider _configFileProvider;
public Mylar(IMylarV3Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_mylarV3Proxy = lidarrV1Proxy;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
try
{
failures.AddIfNotNull(_mylarV3Proxy.TestConnection(Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Mylar"));
}
return new ValidationResult(failures);
}
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _mylarV3Proxy.GetIndexers(Settings);
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
if (indexer.Apikey == _configFileProvider.ApiKey)
{
var match = AppIndexerRegex.Match(indexer.Host);
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(new AppIndexerMap { RemoteIndexerName = $"{indexer.Type},{indexer.Name}", IndexerId = indexerId });
}
}
}
return mappings;
}
public override void AddIndexer(IndexerDefinition indexer)
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
var lidarrIndexer = BuildMylarIndexer(indexer, indexer.Protocol);
var remoteIndexer = _mylarV3Proxy.AddIndexer(lidarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}
}
public override void RemoveIndexer(int indexerId)
{
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
if (indexerMapping != null)
{
//Remove Indexer remotely and then remove the mapping
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
_mylarV3Proxy.RemoveIndexer(indexerProps[1], (MylarProviderType)Enum.Parse(typeof(MylarProviderType), indexerProps[0]), Settings);
_appIndexerMapService.Delete(indexerMapping.Id);
}
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
var mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol, indexerProps[1]);
//Use the old remote id to find the indexer on Mylar incase the update was from a name change in Prowlarr
var remoteIndexer = _mylarV3Proxy.GetIndexer(indexerProps[1], mylarIndexer.Type, Settings);
if (remoteIndexer != null)
{
_logger.Debug("Remote indexer found, syncing with current settings");
if (!mylarIndexer.Equals(remoteIndexer))
{
_mylarV3Proxy.UpdateIndexer(mylarIndexer, Settings);
indexerMapping.RemoteIndexerName = $"{mylarIndexer.Type},{mylarIndexer.Altername}";
_appIndexerMapService.Update(indexerMapping);
}
}
else
{
_appIndexerMapService.Delete(indexerMapping.Id);
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
_logger.Debug("Remote indexer not found, re-adding {0} to Mylar", indexer.Name);
var newRemoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{newRemoteIndexer.Type},{newRemoteIndexer.Name}" });
}
else
{
_logger.Debug("Remote indexer not found for {0}, skipping re-add to Mylar due to indexer capabilities", indexer.Name);
}
}
}
private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
{
var schema = protocol == DownloadProtocol.Usenet ? MylarProviderType.Newznab : MylarProviderType.Torznab;
var lidarrIndexer = new MylarIndexer
{
Name = originalName ?? $"{indexer.Name} (Prowlarr)",
Altername = $"{indexer.Name} (Prowlarr)",
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
Apikey = _configFileProvider.ApiKey,
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
Enabled = indexer.Enable,
Type = schema,
};
return lidarrIndexer;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarError
{
public int Code { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarException : NzbDroneException
{
public MylarException(string message)
: base(message)
{
}
public MylarException(string message, params object[] args)
: base(message, args)
{
}
public MylarException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,22 @@
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarField
{
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
public MylarField Clone()
{
return (MylarField)MemberwiseClone();
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarIndexerResponse
{
public bool Success { get; set; }
public MylarIndexerData Data { get; set; }
public MylarError Error { get; set; }
}
public class MylarIndexerData
{
public List<MylarIndexer> Torznabs { get; set; }
public List<MylarIndexer> Newznabs { get; set; }
}
public enum MylarProviderType
{
Newznab,
Torznab
}
public class MylarIndexer
{
public string Name { get; set; }
public string Host { get; set; }
public string Apikey { get; set; }
public string Categories { get; set; }
public bool Enabled { get; set; }
public string Altername { get; set; }
public MylarProviderType Type { get; set; }
public bool Equals(MylarIndexer other)
{
if (ReferenceEquals(null, other))
{
return false;
}
return other.Host == Host &&
other.Apikey == Apikey &&
other.Name == Name &&
other.Categories == Categories &&
other.Enabled == Enabled &&
other.Altername == Altername;
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarSettingsValidator : AbstractValidator<MylarSettings>
{
public MylarSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class MylarSettings : IApplicationSettings
{
private static readonly MylarSettingsValidator Validator = new MylarSettingsValidator();
public MylarSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8090";
SyncCategories = new[] { NewznabStandardCategory.BooksComics.Id };
}
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Mylar sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Mylar Server", HelpText = "Mylar server URL, including http(s):// and port if needed")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Mylar in Settings/Web Interface")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarStatus
{
public bool Success { get; set; }
public MylarError Error { get; set; }
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Mylar
{
public interface IMylarV3Proxy
{
MylarIndexer AddIndexer(MylarIndexer indexer, MylarSettings settings);
List<MylarIndexer> GetIndexers(MylarSettings settings);
MylarIndexer GetIndexer(string indexerName, MylarProviderType indexerType, MylarSettings settings);
void RemoveIndexer(string indexerName, MylarProviderType indexerType, MylarSettings settings);
MylarIndexer UpdateIndexer(MylarIndexer indexer, MylarSettings settings);
ValidationFailure TestConnection(MylarSettings settings);
}
public class MylarV1Proxy : IMylarV3Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public MylarV1Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public MylarStatus GetStatus(MylarSettings settings)
{
var request = BuildRequest(settings, "/api", "getVersion", HttpMethod.GET);
return Execute<MylarStatus>(request);
}
public List<MylarIndexer> GetIndexers(MylarSettings settings)
{
var request = BuildRequest(settings, "/api", "listProviders", HttpMethod.GET);
var response = Execute<MylarIndexerResponse>(request);
if (!response.Success)
{
throw new MylarException(string.Format("Mylar Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
}
var indexers = new List<MylarIndexer>();
var torIndexers = response.Data.Torznabs;
torIndexers.ForEach(i => i.Type = MylarProviderType.Torznab);
var nzbIndexers = response.Data.Newznabs;
nzbIndexers.ForEach(i => i.Type = MylarProviderType.Newznab);
indexers.AddRange(torIndexers);
indexers.AddRange(nzbIndexers);
indexers.ForEach(i => i.Altername = i.Name);
return indexers;
}
public MylarIndexer GetIndexer(string indexerName, MylarProviderType indexerType, MylarSettings settings)
{
var indexers = GetIndexers(settings);
return indexers.SingleOrDefault(i => i.Name == indexerName && i.Type == indexerType);
}
public void RemoveIndexer(string indexerName, MylarProviderType indexerType, MylarSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexerName },
{ "providertype", indexerType.ToString().ToLower() }
};
var request = BuildRequest(settings, "/api", "delProvider", HttpMethod.GET, parameters);
CheckForError(Execute<MylarStatus>(request));
}
public MylarIndexer AddIndexer(MylarIndexer indexer, MylarSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexer.Name },
{ "providertype", indexer.Type.ToString().ToLower() },
{ "host", indexer.Host },
{ "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories }
};
var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.GET, parameters);
CheckForError(Execute<MylarStatus>(request));
return indexer;
}
public MylarIndexer UpdateIndexer(MylarIndexer indexer, MylarSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexer.Name },
{ "providertype", indexer.Type.ToString().ToLower() },
{ "host", indexer.Host },
{ "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories },
{ "altername", indexer.Altername }
};
var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.GET, parameters);
CheckForError(Execute<MylarStatus>(request));
return indexer;
}
private void CheckForError(MylarStatus response)
{
if (!response.Success)
{
throw new MylarException(string.Format("Mylar Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
}
}
public ValidationFailure TestConnection(MylarSettings settings)
{
try
{
var status = GetStatus(settings);
if (!status.Success)
{
return new ValidationFailure("ApiKey", status.Error.Message);
}
}
catch (HttpException ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(MylarSettings settings, string resource, string command, HttpMethod method, Dictionary<string, string> parameters = null)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var requestBuilder = new HttpRequestBuilder(baseUrl).Resource(resource)
.AddQueryParam("cmd", command)
.AddQueryParam("apikey", settings.ApiKey);
if (parameters != null)
{
foreach (var param in parameters)
{
requestBuilder.AddQueryParam(param.Key, param.Value);
}
}
var request = requestBuilder.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
request.AllowAutoRedirect = true;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}

View File

@@ -55,12 +55,12 @@ namespace NzbDrone.Core.Applications.Radarr
return new ValidationResult(failures);
}
public override Dictionary<int, int> GetIndexerMappings()
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _radarrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new Dictionary<int, int>();
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Applications.Radarr
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(indexer.Id, indexerId);
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}

View File

@@ -55,12 +55,12 @@ namespace NzbDrone.Core.Applications.Readarr
return new ValidationResult(failures);
}
public override Dictionary<int, int> GetIndexerMappings()
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _readarrV1Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new Dictionary<int, int>();
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(indexer.Id, indexerId);
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}

View File

@@ -55,12 +55,12 @@ namespace NzbDrone.Core.Applications.Sonarr
return new ValidationResult(failures);
}
public override Dictionary<int, int> GetIndexerMappings()
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _sonarrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new Dictionary<int, int>();
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Applications.Sonarr
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(indexer.Id, indexerId);
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}
@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Applications.Sonarr
public override void AddIndexer(IndexerDefinition indexer)
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
{
var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol);
@@ -128,7 +128,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{
_appIndexerMapService.Delete(indexerMapping.Id);
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
{
_logger.Debug("Remote indexer not found, re-adding {0} to Sonarr", indexer.Name);
sonarrIndexer.Id = 0;
@@ -169,7 +169,7 @@ namespace NzbDrone.Core.Applications.Sonarr
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 == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()));
return sonarrIndexer;
}

View File

@@ -23,10 +23,12 @@ namespace NzbDrone.Core.Applications.Sonarr
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8989";
SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050, 5070 };
SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050 };
AnimeSyncCategories = new[] { 5070 };
}
public IEnumerable<int> SyncCategories { get; set; }
public IEnumerable<int> AnimeSyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }

View File

@@ -1,11 +1,7 @@
using System;
using System.Linq;
using System.Xml.Linq;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Authentication
{

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;

View File

@@ -1,5 +1,4 @@
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration

View File

@@ -0,0 +1,63 @@
using System.Data;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(8)]
public class redacted_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 = 'Redacted'";
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();
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE Indexers SET Settings = ?, ConfigContract = ?, Enable = 0 WHERE Id = ?";
updateCmd.AddParameter(settings);
updateCmd.AddParameter("RedactedSettings");
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(10)]
public class IndexerProxies : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("IndexerProxies")
.WithColumn("Name").AsString()
.WithColumn("Settings").AsString()
.WithColumn("Implementation").AsString()
.WithColumn("ConfigContract").AsString().Nullable()
.WithColumn("Tags").AsString().Nullable();
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
}
}
}

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