Compare commits

...

194 Commits

Author SHA1 Message Date
bakerboy448 0cca9525a1 Fixed: (Indexers) Download requests not using the configured proxy
Fixes #520
2021-10-06 17:30:37 -05:00
Servarr 3455f3c92a Translations from Weblate
Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Arabic)

Currently translated at 81.6% (360 of 441 strings)

Translated using Weblate (Russian)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Icelandic)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Danish)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Slovak)

Currently translated at 10.6% (47 of 441 strings)

Translated using Weblate (German)

Currently translated at 95.4% (421 of 441 strings)

Translated using Weblate (Korean)

Currently translated at 60.3% (266 of 441 strings)

Translated using Weblate (Japanese)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Catalan)

Currently translated at 2.4% (11 of 441 strings)

Translated using Weblate (Bulgarian)

Currently translated at 75.7% (334 of 441 strings)

Translated using Weblate (Thai)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Romanian)

Currently translated at 81.8% (361 of 441 strings)

Translated using Weblate (Italian)

Currently translated at 85.7% (378 of 441 strings)

Translated using Weblate (Hindi)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Spanish)

Currently translated at 83.2% (367 of 441 strings)

Translated using Weblate (Hebrew)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Portuguese)

Currently translated at 84.1% (371 of 441 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 1.1% (5 of 441 strings)

Translated using Weblate (Swedish)

Currently translated at 81.8% (361 of 441 strings)

Translated using Weblate (Czech)

Currently translated at 81.4% (359 of 441 strings)

Translated using Weblate (Greek)

Currently translated at 81.1% (358 of 441 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 12.4% (55 of 441 strings)

Translated using Weblate (Turkish)

Currently translated at 81.6% (360 of 441 strings)

Translated using Weblate (Vietnamese)

Currently translated at 81.4% (359 of 441 strings)

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

Currently translated at 90.9% (401 of 441 strings)

Translated using Weblate (Polish)

Currently translated at 81.4% (359 of 441 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_Hans/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
2021-10-05 08:34:23 -05:00
Weblate af03c17892 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2021-10-02 11:18:49 -04:00
Weblate 6b39fa5ce6 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2021-10-02 10:36:09 -04:00
Servarr 1ee79f16fc Translated using Weblate (Turkish) (#516)
Currently translated at 16.5% (73 of 441 strings)

Translated using Weblate (French)

Currently translated at 99.7% (440 of 441 strings)

Co-authored-by: Francis Peixoto <peixoto.francis@gmail.com>
Co-authored-by: Micky <ad312@pm.me>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Francis Peixoto <peixoto.francis@gmail.com>
Co-authored-by: Micky <ad312@pm.me>
2021-10-01 15:17:59 -05:00
bakerboy448 e73f2466cc Fixed (Headphones): Add missing description 2021-10-01 15:17:35 -05:00
Weblate 2e56b7681e Translated using Weblate (Dutch)
Currently translated at 100.0% (441 of 441 strings)

Translated using Weblate (Bulgarian)

Currently translated at 10.2% (45 of 441 strings)

Translated using Weblate (Korean)

Currently translated at 17.9% (79 of 441 strings)

Translated using Weblate (Spanish)

Currently translated at 76.1% (336 of 441 strings)

Co-authored-by: COTMO <moermantom1@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: eslifos <eaaf_01@hotmail.com>
Co-authored-by: keysuck <joshkkim@gmail.com>
Co-authored-by: siankatabg <siankata91@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translation: Servarr/Prowlarr
2021-09-25 22:08:27 -05:00
Jayson Reis 87650c83c6 New: (Indexer) lat-team.com (#505)
* New: (Indexer) Torrent lat-team.com

* Add lat-team to ignore definitions list

* Fix lat-team name
2021-09-25 22:03:42 -05:00
Qstick 34a09af01e New: Advanced settings for Application category control 2021-09-25 09:46:31 -04:00
Qstick 5a3d429d52 Remove unused MovieMonitor input 2021-09-25 09:46:31 -04:00
Qstick 40c49bce9b Fixed: Spinning of Test All Indexer icon
#507
2021-09-23 20:42:17 -05:00
Qstick 063083a1f1 Fixed: Search from header on Search Page
Fixes #511
2021-09-23 20:28:22 -05:00
bakerboy448 dbbc913809 New: (InternetArchive) Add Support for TV Categories
ref API docs `movies: any item where the main media content is video files, like mpeg, mov, avi, etc`
2021-09-19 08:47:59 -05:00
Qstick f0f2c88c4a Fixed: (qBittorrent) Don't fail if remove torrents is enabled
We don't care about this in Prowlarr

Fixes #499
2021-09-15 22:04:09 -05:00
Weblate 1bfcb99f31 Translated using Weblate (Slovak)
Currently translated at 7.4% (33 of 441 strings)

Added translation using Weblate (Slovak)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 9.2% (41 of 441 strings)

Added translation using Weblate (Norwegian Bokmål)

Translated using Weblate (Portuguese)

Currently translated at 83.6% (369 of 441 strings)

Translated using Weblate (Dutch)

Currently translated at 78.9% (348 of 441 strings)

Co-authored-by: 7even <henning@wikene.no>
Co-authored-by: Samuel Bartík <github.fundal@aleeas.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: sergioquiterio <serquiterio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translation: Servarr/Prowlarr
2021-09-15 21:57:50 -05:00
bakerboy448 4ea0e6c016 add various missing descriptions 2021-09-15 21:57:26 -05:00
bakerboy448 1de845c8f5 (indexers) various - standardized language format with {ISO 639-1}-{ISO 3166-1 alpha-2} 2021-09-15 21:57:26 -05:00
bakerboy448 f3a33cf817 Fixed: Missing Proxy Validation Translations 2021-09-15 21:43:51 -05:00
bakerboy448 593a0e9658 Fixed: (MAM) Don't Parse HTTP Error Responses 2021-09-07 16:03:34 -05:00
Servarr a854ce6f4e Translated using Weblate (Portuguese (Brazil)) (#484)
Currently translated at 100.0% (441 of 441 strings)

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

Currently translated at 62.5% (276 of 441 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (441 of 441 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
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: angelsky11 <angelsky11@gmail.com>
2021-09-03 14:33:08 -05:00
bakerboy448 043b1a0e46 Fixed: Better Log Cleansing
Ref #280 YGG Comment
2021-08-31 19:51:37 -04:00
bakerboy448 4c7c7e8a62 Fixed: (BTN) Daily Episode Searches failing
Fixes #480
2021-08-31 19:50:46 -04:00
Qstick 89a4c03dd2 Fixed: Translations for Tags setting page
Fixes #471
2021-08-30 22:48:24 -04:00
Qstick baed2960b6 Fixed: (TorrentSeeds) new Login
Fixes #460
2021-08-30 22:39:05 -04:00
Qstick e4ef1c3af0 Fixed: Convert DesiTorrents to Gazelle
Fixes #220
2021-08-30 22:20:40 -04:00
bakerboy448 b4f8fb733f Fixed: Indexer Display Issue on Search Page
Fixes #177
2021-08-30 20:47:46 -04:00
bakerboy448 1a6ea21b9f Fixed: Increased Xthor Rate Limit Time
Fixes Logs of #472

> 2021-08-28 05:59:47.8|Error|Xthor|An error occurred while processing indexer feed. https://api.xthor.tk?passkey=(removed)&category=12%2B125%2B104%2B13%2B15%2B14%2B98%2B17%2B16%2B101%2B32%2B110%2B123%2B109%2B34%2B30&accent=1

[v0.1.1.875] System.Exception: Triggered AntiSpam Protection, please delay your requests!
   at NzbDrone.Core.Indexers.Definitions.Xthor.XthorParser.CheckApiState(XthorError state) in D:\a\1\s\src\NzbDrone.Core\Indexers\Definitions\Xthor\XthorParser.cs:line 112
   at NzbDrone.Core.Indexers.Definitions.Xthor.XthorParser.ParseResponse(IndexerResponse indexerResponse) in D:\a\1\s\src\NzbDrone.Core\Indexers\Definitions\Xthor\XthorParser.cs:line 32
   at NzbDrone.Core.Indexers.HttpIndexerBase`1.FetchPage(IndexerRequest request, IParseIndexerResponse parser) in D:\a\1\s\src\NzbDrone.Core\Indexers\HttpIndexerBase.cs:line 321
   at NzbDrone.Core.Indexers.HttpIndexerBase`1.FetchReleases(Func`2 pageableRequestChainSelector, Boolean isRecent) in D:\a\1\s\src\NzbDrone.Core\Indexers\HttpIndexerBase.cs:line 174
RequestUri: https://api.xthor.tk?passkey=(removed)&category=12%2B125%2B104%2B13%2B15%2B14%2B98%2B17%2B16%2B101%2B32%2B110%2B123%2B109%2B34%2B30&accent=1;StatusCode: OK;ContentType: application/json;ContentLength: 133;ContentSample: {
    "error": {
        "code": 8,
        "descr": "Protection Anti-Spam: A.P.I limitee a 1 requete toutes les 2 secondes."
    }
};FeedUrl: https://api.xthor.tk?passkey=(removed)&category=12%2B125%2B104%2B13%2B15%2B14%2B98%2B17%2B16%2B101%2B32%2B110%2B123%2B109%2B34%2B30&accent=1
2021-08-30 19:58:48 -04:00
bakerboy448 16834e0f24 Fixed: (TPB) No Results returned for Torznab searches
Fixed: (TPB) Missing Magnet Download Link
2021-08-30 19:43:20 -04:00
Qstick 658724b315 OpenAPI auto generation test 2021-08-29 23:12:59 -04:00
bakerboy448 a2c8cec27e bug template fix [skip ci] [common] 2021-08-27 20:46:58 -05:00
Nyuels 3c9fbeabaa Fixed: Use Gazelle freelech tokens. (#465) 2021-08-27 18:21:56 -04:00
Weblate 04e84f3a90 Translated using Weblate (Hungarian)
Currently translated at 100.0% (438 of 438 strings)

Translated using Weblate (French)

Currently translated at 97.7% (428 of 438 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (438 of 438 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translation: Servarr/Prowlarr
2021-08-25 23:23:36 -04:00
Steve Adams 77a76fe5a1 New: HDBits to parse IMDB using parser utils (#454) 2021-08-25 23:22:04 -04:00
ta264 1d20b9d429 Fixed: Don't delete tags used by indexer proxies 2021-08-23 22:12:40 +01:00
bakerboy448 46e1cce632 New: (TorrentLeech) Add VIP Expiration
Closes #419
bonus- sort en.json
2021-08-23 22:20:41 -05:00
Qstick 03f821f484 New: Make VIP Check Generic 2021-08-23 22:35:28 -04:00
Qstick c72222a696 Mylar code cleanup 2021-08-22 22:24:46 -04:00
bakerboy448 f4cee1d5f4 Fixed: UserAgent Parsing
Fixes #448

New - add user agent parser tests
2021-08-22 15:00:59 -05:00
Qstick ab7bc85368 fix mass delete 2021-08-22 15:53:30 -04:00
bakerboy448 d50e1d7cc0 update readme [skip ci] 2021-08-22 10:25:37 -05:00
Qstick ab1545e834 Fixed: Mass Indexer delete fails with 415 2021-08-22 11:04:00 -04:00
Weblate c8cc48229c Translated using Weblate (French)
Currently translated at 97.9% (429 of 438 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (438 of 438 strings)

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: ProBatou <baptiste2105@hotmail.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2021-08-21 23:00:33 -04:00
Steve Adams b513fac2f7 Fixed: (HDBits) Not parsing the search term for TV (#444)
* Fixing HDBits not parsing the search term

* remove debug vars

* Handle TVDB Properly
2021-08-21 22:59:53 -04:00
Qstick 368e0755a0 Update README.md
[skip ci]
2021-08-21 22:56:50 -04:00
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
bakerboy448 dfb254d2dc Fixed: Default Branch is now Develop 2021-06-26 06:36:55 -05:00
Weblate 07c03b0a12 Translated using Weblate (Portuguese)
Currently translated at 83.7% (356 of 425 strings)

Translated using Weblate (German)

Currently translated at 92.7% (394 of 425 strings)

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@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/pt/
Translation: Servarr/Prowlarr
2021-06-26 07:14:03 -04:00
bakerboy448 eb0cf2d5f6 bug update [skip ci] 2021-06-26 05:48:17 -05:00
Servarr 69c04ebe7a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (Portuguese)

Currently translated at 83.7% (356 of 425 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 0.9% (4 of 425 strings)

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_Hans/
Translation: Servarr/Prowlarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
2021-06-25 20:02:41 -05:00
bakerboy448 135db6d2ff Fixed: Cleanse additional AuthKey instances in logs 2021-06-24 19:27:32 -05:00
bakerboy448 c3deace9e6 Fixed: Incorrectly cleansing usetoken param 2021-06-24 18:56:15 -05:00
Qstick 9042594b14 Update FUNDING.yml to include GitHub sponsors 2021-06-24 19:52:20 -04:00
bakerboy448 44df0f5c3d Fixed: NZBGet Settings hint mentions Sabnzbd 2021-06-24 11:02:23 -05:00
bakerboy448 7b9446eb35 fix template formatting [skip ci] 2021-06-23 23:55:10 -05:00
345 changed files with 27468 additions and 8133 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: Prowlarr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: prowlarr open_collective: prowlarr
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
-37
View File
@@ -1,37 +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-->
+75
View File
@@ -0,0 +1,75 @@
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!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true
-20
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. -->
@@ -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
+4 -3
View File
@@ -1,14 +1,15 @@
#### Database Migration #### Database Migration
YES | NO YES - XXXX | NO
#### Description #### Description
A few sentences describing the overall goals of the pull request's commits. A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related) #### Screenshot (if UI related)
#### Todos #### Todos
- [ ] Tests - [ ] Tests
- [ ] Translation Keys - [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
- [ ] Wiki Updates - [ ] [Wiki Updates](https://wiki.servarr.com)
#### Issues Fixed or Closed by this PR #### Issues Fixed or Closed by this PR
+41
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
+3 -1
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. 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 ## ## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better. 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 ### ### 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) - 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 - 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 - 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 - Reach out to us on the discord if you have any questions
+12 -11
View File
@@ -7,17 +7,18 @@
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors) [![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors)
Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Sonarr, Radarr, Lidarr, and Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all). Prowlarr is an indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
## Major Features Include: ## Major Features Include:
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab" - 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" - 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 Sync to Sonarr/Radarr/Readarr/Lidarr/Mylar3, so no manual configuration of the other applications are required
- Indexer History and Statistics - Indexer history and statistics
- Manual Searching of Trackers & Indexers at a category level - Manual searching of Trackers & Indexers at a category level
- Support for pushing releases directly to your download clients from Prowlarr - Support for pushing releases directly to your download clients from Prowlarr
- Indexer health and status notifications - Indexer health and status notifications
- Per Indexer proxy support (SOCKS4, SOCKS5, HTTP, Flaresolverr)
## Support ## Support
Note: Prowlarr is currently early in life, thus bugs should be expected Note: Prowlarr is currently early in life, thus bugs should be expected
@@ -34,14 +35,14 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
[Indexer Requests](https://requests.prowlarr.com) [Indexer Requests](https://requests.prowlarr.com)
- Request or vote on an existing request for a new tracker/indexer - 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 ## 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>
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/prowlarr/contributing)
- [YML Indexer Defintion (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
This project exists thanks to all the people who contribute.
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
## Backers ## Backers
+3 -3
View File
@@ -7,13 +7,13 @@ variables:
outputFolder: './_output' outputFolder: './_output'
artifactsFolder: './_artifacts' artifactsFolder: './_artifacts'
testsFolder: './_tests' testsFolder: './_tests'
majorVersion: '0.1.0' majorVersion: '0.1.1'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.203' dotnetVersion: '5.0.400'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger: trigger:
@@ -879,7 +879,7 @@ stages:
artifactName: 'WindowsAutomationScreenshots' artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory) targetPath: $(Build.SourcesDirectory)
- checkout: none - checkout: none
- powershell: | - pwsh: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1')) iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env: env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+6
View File
@@ -11,6 +11,7 @@ import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSetti
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings'; import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings'; import TagSettings from 'Settings/Tags/TagSettings';
@@ -90,6 +91,11 @@ function AppRoutes(props) {
component={Settings} component={Settings}
/> />
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route <Route
path="/settings/applications" path="/settings/applications"
component={ApplicationSettingsConnector} component={ApplicationSettingsConnector}
@@ -16,7 +16,6 @@ import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import InfoInput from './InfoInput'; import InfoInput from './InfoInput';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
@@ -69,9 +68,6 @@ function getComponent(type) {
case inputTypes.PATH: case inputTypes.PATH:
return PathInputConnector; return PathInputConnector;
case inputTypes.MOVIE_MONITORED_SELECT:
return MovieMonitoredSelectInput;
case inputTypes.INDEXER_FLAGS_SELECT: case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector; return IndexerFlagsSelectInputConnector;
@@ -11,8 +11,6 @@ class InfoInput extends Component {
value value
} = this.props; } = this.props;
console.log(this.props);
return ( return (
<span dangerouslySetInnerHTML={{ __html: value }} /> <span dangerouslySetInnerHTML={{ __html: value }} />
); );
@@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import SelectInput from './SelectInput';
const monitorTypesOptions = [
{ key: 'true', value: 'True' },
{ key: 'false', value: 'False' }
];
function MovieMonitoredSelectInput(props) {
const values = [...monitorTypesOptions];
const {
includeNoChange,
includeMixed
} = props;
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
});
}
return (
<SelectInput
{...props}
values={values}
/>
);
}
MovieMonitoredSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired
};
MovieMonitoredSelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default MovieMonitoredSelectInput;
@@ -53,7 +53,8 @@ function getSelectValues(selectOptions) {
result.push({ result.push({
key: option.value, key: option.value,
value: option.name, value: option.name,
hint: option.hint hint: option.hint,
parentKey: option.parentValue
}); });
return result; return result;
@@ -60,7 +60,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onGoToAddNewMovie(query) { onGoToAddNewMovie(query) {
dispatch(setSearchDefault({ searchQuery: query, searchIndexerIds: [-1, -2] })); dispatch(setSearchDefault({ searchQuery: query }));
dispatch(push(`${window.Prowlarr.urlBase}/search`)); dispatch(push(`${window.Prowlarr.urlBase}/search`));
} }
}; };
@@ -48,6 +48,10 @@ const links = [
title: translate('Settings'), title: translate('Settings'),
to: '/settings', to: '/settings',
children: [ children: [
{
title: translate('Indexers'),
to: '/settings/indexers'
},
{ {
title: translate('Apps'), title: translate('Apps'),
to: '/settings/applications' to: '/settings/applications'
+2 -2
View File
@@ -168,9 +168,9 @@ class SignalRConnector extends Component {
this.props.dispatchFetchIndexerStatus(); this.props.dispatchFetchIndexerStatus();
} }
handleMovie = (body) => { handleIndexer = (body) => {
const action = body.action; const action = body.action;
const section = 'movies'; const section = 'indexers';
if (action === 'updated') { if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource }); this.props.dispatchUpdateItem({ section, ...body.resource });
+1 -1
View File
@@ -228,4 +228,4 @@ export const UNSAVED_SETTING = farDotCircle;
export const VIEW = fasEye; export const VIEW = fasEye;
export const WARNING = fasExclamationTriangle; export const WARNING = fasExclamationTriangle;
export const WIKI = fasBookReader; export const WIKI = fasBookReader;
export const BLACKLIST = fasBan; export const BLOCKLIST = fasBan;
@@ -13,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css'; import styles from './EditIndexerModalContent.css';
@@ -31,6 +32,7 @@ function EditIndexerModalContent(props) {
onSavePress, onSavePress,
onTestPress, onTestPress,
onDeleteIndexerPress, onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps ...otherProps
} = props; } = props;
@@ -43,6 +45,7 @@ function EditIndexerModalContent(props) {
supportsRss, supportsRss,
supportsRedirect, supportsRedirect,
appProfileId, appProfileId,
tags,
fields, fields,
priority priority
} = item; } = item;
@@ -87,7 +90,6 @@ function EditIndexerModalContent(props) {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="enable" name="enable"
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')} helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enable} {...enable}
onChange={onInputChange} onChange={onInputChange}
/> />
@@ -150,6 +152,18 @@ function EditIndexerModalContent(props) {
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </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> </Form>
} }
</ModalBody> </ModalBody>
@@ -165,6 +179,12 @@ function EditIndexerModalContent(props) {
</Button> </Button>
} }
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton <SpinnerErrorButton
isSpinning={isTesting} isSpinning={isTesting}
error={saveError} error={saveError}
@@ -204,6 +224,7 @@ EditIndexerModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired, onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func onDeleteIndexerPress: PropTypes.func
}; };
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions'; import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector'; import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent'; import EditIndexerModalContent from './EditIndexerModalContent';
@@ -23,7 +24,8 @@ const mapDispatchToProps = {
setIndexerValue, setIndexerValue,
setIndexerFieldValue, setIndexerFieldValue,
saveIndexer, saveIndexer,
testIndexer testIndexer,
toggleAdvancedSettings
}; };
class EditIndexerModalContentConnector extends Component { class EditIndexerModalContentConnector extends Component {
@@ -56,6 +58,10 @@ class EditIndexerModalContentConnector extends Component {
this.props.testIndexer({ id: this.props.id }); this.props.testIndexer({ id: this.props.id });
} }
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
}
// //
// Render // Render
@@ -65,6 +71,7 @@ class EditIndexerModalContentConnector extends Component {
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange} onFieldChange={this.onFieldChange}
/> />
@@ -80,6 +87,7 @@ EditIndexerModalContentConnector.propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired, setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired, setIndexerFieldValue: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired, saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired, testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
+3 -1
View File
@@ -270,6 +270,7 @@ class IndexerIndex extends Component {
isSaving, isSaving,
saveError, saveError,
isDeleting, isDeleting,
isTestingAll,
deleteError, deleteError,
onScroll, onScroll,
onSortSelect, onSortSelect,
@@ -310,7 +311,7 @@ class IndexerIndex extends Component {
<PageToolbarButton <PageToolbarButton
label={'Test All Indexers'} label={'Test All Indexers'}
iconName={icons.TEST} iconName={icons.TEST}
spinningName={icons.TEST} isSpinning={isTestingAll}
isDisabled={hasNoIndexer} isDisabled={hasNoIndexer}
onPress={this.props.onTestAllPress} onPress={this.props.onTestAllPress}
/> />
@@ -489,6 +490,7 @@ IndexerIndex.propTypes = {
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
isTestingAll: PropTypes.bool.isRequired,
deleteError: PropTypes.object, deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
@@ -71,7 +71,7 @@ class IndexerIndexRow extends Component {
const { const {
id, id,
name, name,
baseUrl, indexerUrls,
enable, enable,
redirect, redirect,
tags, tags,
@@ -248,7 +248,7 @@ class IndexerIndexRow extends Component {
className={styles.externalLink} className={styles.externalLink}
name={icons.EXTERNAL_LINK} name={icons.EXTERNAL_LINK}
title={'Website'} title={'Website'}
to={baseUrl.replace('api.', '')} to={indexerUrls[0].replace('api.', '')}
/> />
<IconButton <IconButton
@@ -289,7 +289,7 @@ class IndexerIndexRow extends Component {
IndexerIndexRow.propTypes = { IndexerIndexRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
baseUrl: PropTypes.string.isRequired, indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired, privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired, priority: PropTypes.number.isRequired,
@@ -31,7 +31,7 @@ function IndexerStatusCell(props) {
<Icon <Icon
className={styles.statusIcon} className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT} kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon: icons.BLACKLIST} name={enabled ? enableIcon: icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'} title={enabled ? enableTitle : 'Indexer is Disabled'}
/> />
} }
@@ -18,7 +18,7 @@ function IndexerInfoModalContent(props) {
description, description,
encoding, encoding,
language, language,
baseUrl, indexerUrls,
protocol, protocol,
onModalClose onModalClose
} = props; } = props;
@@ -54,10 +54,10 @@ function IndexerInfoModalContent(props) {
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle> <DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to={baseUrl}>{baseUrl}</Link> <Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle>{protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url</DescriptionListItemTitle> <DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`} {`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription> </DescriptionListItemDescription>
@@ -74,7 +74,7 @@ IndexerInfoModalContent.propTypes = {
description: PropTypes.string.isRequired, description: PropTypes.string.isRequired,
encoding: PropTypes.string.isRequired, encoding: PropTypes.string.isRequired,
language: PropTypes.string.isRequired, language: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired, indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -10,7 +10,6 @@ function createMapStateToProps() {
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createIndexerSelector(), createIndexerSelector(),
(advancedSettings, indexer) => { (advancedSettings, indexer) => {
console.log(indexer);
return { return {
advancedSettings, advancedSettings,
...indexer ...indexer
@@ -26,7 +26,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Protocol {translate('Protocol')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -35,7 +35,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Age {translate('Age')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -53,7 +53,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Indexer {translate('Indexer')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -62,7 +62,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Size {translate('Size')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -71,7 +71,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Files {translate('Files')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -80,7 +80,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Grabs {translate('Grabs')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -89,7 +89,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Peers {translate('Peers')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -98,7 +98,7 @@ function SearchIndexSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
Category {translate('Category')}
</SortMenuItem> </SortMenuItem>
</MenuContent> </MenuContent>
</SortMenu> </SortMenu>
+1 -1
View File
@@ -19,7 +19,7 @@ function NoSearchResults(props) {
return ( return (
<div> <div>
<div className={styles.message}> <div className={styles.message}>
No search results found, try performing a new search below. {translate('NoSearchResultsFound')}
</div> </div>
</div> </div>
); );
+13 -9
View File
@@ -7,6 +7,7 @@ import TextInput from 'Components/Form/TextInput';
import keyboardShortcuts from 'Components/keyboardShortcuts'; import keyboardShortcuts from 'Components/keyboardShortcuts';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import translate from 'Utilities/String/translate';
import SearchFooterLabel from './SearchFooterLabel'; import SearchFooterLabel from './SearchFooterLabel';
import styles from './SearchFooter.css'; import styles from './SearchFooter.css';
@@ -26,7 +27,7 @@ class SearchFooter extends Component {
this.state = { this.state = {
searchingReleases: false, searchingReleases: false,
searchQuery: defaultSearchQuery, searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds, searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories searchCategories: defaultCategories
}; };
@@ -57,12 +58,15 @@ class SearchFooter extends Component {
const { const {
searchIndexerIds, searchIndexerIds,
searchCategories, searchCategories
searchQuery
} = this.state; } = this.state;
const newState = {}; const newState = {};
if (defaultSearchQuery && defaultSearchQuery !== prevProps.defaultSearchQuery) {
newState.searchQuery = defaultSearchQuery;
}
if (searchIndexerIds !== defaultIndexerIds) { if (searchIndexerIds !== defaultIndexerIds) {
newState.searchIndexerIds = defaultIndexerIds; newState.searchIndexerIds = defaultIndexerIds;
} }
@@ -71,10 +75,6 @@ class SearchFooter extends Component {
newState.searchCategories = defaultCategories; newState.searchCategories = defaultCategories;
} }
if (searchQuery !== defaultSearchQuery) {
newState.searchQuery = defaultSearchQuery;
}
if (prevProps.isFetching && !isFetching && !searchError) { if (prevProps.isFetching && !isFetching && !searchError) {
newState.searchingReleases = false; newState.searchingReleases = false;
} }
@@ -91,6 +91,10 @@ class SearchFooter extends Component {
this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories); this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories);
} }
onSearchInputChange = ({ value }) => {
this.setState({ searchQuery: value });
}
// //
// Render // Render
@@ -120,7 +124,7 @@ class SearchFooter extends Component {
autoFocus={true} autoFocus={true}
value={searchQuery} value={searchQuery}
isDisabled={isFetching} isDisabled={isFetching}
onChange={onInputChange} onChange={this.onSearchInputChange}
/> />
</div> </div>
@@ -167,7 +171,7 @@ class SearchFooter extends Component {
isDisabled={isFetching || !hasIndexers} isDisabled={isFetching || !hasIndexers}
onPress={this.onSearchPress} onPress={this.onSearchPress}
> >
Search {translate('Search')}
</SpinnerButton> </SpinnerButton>
</div> </div>
</div> </div>
@@ -1,10 +1,22 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Tooltip from '../../Components/Tooltip/Tooltip';
function CategoryLabel({ categories }) { function CategoryLabel({ categories }) {
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id); 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 ( return (
<span> <span>
{ {
@@ -20,6 +32,10 @@ function CategoryLabel({ categories }) {
); );
} }
CategoryLabel.defaultProps = {
categories: []
};
CategoryLabel.propTypes = { CategoryLabel.propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired categories: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
@@ -10,7 +10,8 @@ import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) { function AdvancedSettingsButton(props) {
const { const {
advancedSettings, advancedSettings,
onAdvancedSettingsPress onAdvancedSettingsPress,
showLabel
} = props; } = props;
return ( return (
@@ -43,18 +44,27 @@ function AdvancedSettingsButton(props) {
/> />
</span> </span>
<div className={styles.labelContainer}> {
<div className={styles.label}> showLabel &&
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')} <div className={styles.labelContainer}>
</div> <div className={styles.label}>
</div> {advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
</div>
</div>
}
</Link> </Link>
); );
} }
AdvancedSettingsButton.propTypes = { AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired onAdvancedSettingsPress: PropTypes.func.isRequired,
showLabel: PropTypes.bool.isRequired
};
AdvancedSettingsButton.defaultProps = {
showLabel: true
}; };
export default AdvancedSettingsButton; export default AdvancedSettingsButton;
@@ -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';
}
}
@@ -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;
@@ -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;
@@ -0,0 +1,5 @@
.indexerProxies {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
@@ -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;
@@ -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);
@@ -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;
@@ -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;
@@ -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);
@@ -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;
}
@@ -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;
@@ -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);
@@ -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;
}
@@ -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;
@@ -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);
@@ -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;
}
@@ -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;
@@ -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;
+15 -4
View File
@@ -16,13 +16,24 @@ function Settings() {
<PageContentBody> <PageContentBody>
<Link <Link
className={styles.link} className={styles.link}
to="/settings/applications" to="/settings/indexers"
> >
Applications {translate('Indexers')}
</Link> </Link>
<div className={styles.summary}> <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> </div>
<Link <Link
@@ -40,7 +51,7 @@ function Settings() {
className={styles.link} className={styles.link}
to="/settings/connect" to="/settings/connect"
> >
Notifications {translate('Notifications')}
</Link> </Link>
<div className={styles.summary}> <div className={styles.summary}>
@@ -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;
@@ -1,26 +1,22 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Label from 'Components/Label';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import split from 'Utilities/String/split';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
import styles from './TagDetailsModalContent.css'; import styles from './TagDetailsModalContent.css';
function TagDetailsModalContent(props) { function TagDetailsModalContent(props) {
const { const {
label, label,
isTagUsed, isTagUsed,
movies, indexers,
delayProfiles,
notifications, notifications,
restrictions, indexerProxies,
onModalClose, onModalClose,
onDeleteTagPress onDeleteTagPress
} = props; } = props;
@@ -40,13 +36,13 @@ function TagDetailsModalContent(props) {
} }
{ {
!!movies.length && !!indexers.length &&
<FieldSet legend={translate('Movies')}> <FieldSet legend={translate('Indexers')}>
{ {
movies.map((item) => { indexers.map((item) => {
return ( return (
<div key={item.id}> <div key={item.id}>
{item.title} {item.name}
</div> </div>
); );
}) })
@@ -54,35 +50,6 @@ function TagDetailsModalContent(props) {
</FieldSet> </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 && !!notifications.length &&
<FieldSet legend={translate('Connections')}> <FieldSet legend={translate('Connections')}>
@@ -99,44 +66,13 @@ function TagDetailsModalContent(props) {
} }
{ {
!!restrictions.length && !!indexerProxies.length &&
<FieldSet legend={translate('Restrictions')}> <FieldSet legend={translate('Indexer Proxies')}>
{ {
restrictions.map((item) => { indexerProxies.map((item) => {
return ( return (
<div <div key={item.id}>
key={item.id} {item.name}
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> </div>
); );
}) })
@@ -171,10 +107,9 @@ function TagDetailsModalContent(props) {
TagDetailsModalContent.propTypes = { TagDetailsModalContent.propTypes = {
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
isTagUsed: PropTypes.bool.isRequired, isTagUsed: PropTypes.bool.isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired, indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: 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, onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired onDeleteTagPress: PropTypes.func.isRequired
}; };
@@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import TagDetailsModalContent from './TagDetailsModalContent'; import TagDetailsModalContent from './TagDetailsModalContent';
function findMatchingItems(ids, items) { function findMatchingItems(ids, items) {
@@ -9,35 +8,15 @@ function findMatchingItems(ids, items) {
}); });
} }
function createUnorderedMatchingMoviesSelector() { function createMatchingIndexersSelector() {
return createSelector( return createSelector(
(state, { indexerIds }) => indexerIds, (state, { indexerIds }) => indexerIds,
createAllIndexersSelector(), (state) => state.indexers.items,
findMatchingItems findMatchingItems
); );
} }
function createMatchingMoviesSelector() { function createMatchingIndexerProxiesSelector() {
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() {
return createSelector( return createSelector(
(state, { notificationIds }) => notificationIds, (state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items, (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() { function createMapStateToProps() {
return createSelector( return createSelector(
createMatchingMoviesSelector(), createMatchingIndexersSelector(),
createMatchingIndexerProxiesSelector(),
createMatchingNotificationsSelector(), createMatchingNotificationsSelector(),
(movies, notifications) => { (indexers, indexerProxies, notifications) => {
return { return {
movies, indexers,
indexerProxies,
notifications notifications
}; };
} }
+16 -40
View File
@@ -53,11 +53,9 @@ class Tag extends Component {
render() { render() {
const { const {
label, label,
delayProfileIds,
notificationIds, notificationIds,
restrictionIds, indexerIds,
importListIds, indexerProxyIds
movieIds
} = this.props; } = this.props;
const { const {
@@ -66,11 +64,9 @@ class Tag extends Component {
} = this.state; } = this.state;
const isTagUsed = !!( const isTagUsed = !!(
delayProfileIds.length || indexerIds.length ||
notificationIds.length || notificationIds.length ||
restrictionIds.length || indexerProxyIds.length
importListIds.length ||
movieIds.length
); );
return ( return (
@@ -87,37 +83,23 @@ class Tag extends Component {
isTagUsed && isTagUsed &&
<div> <div>
{ {
!!movieIds.length && !!indexerIds.length &&
<div> <div>
{movieIds.length} movies {indexerIds.length} {indexerIds.length > 1 ? translate('Indexers') : translate('Indexer')}
</div>
}
{
!!delayProfileIds.length &&
<div>
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
</div> </div>
} }
{ {
!!notificationIds.length && !!notificationIds.length &&
<div> <div>
{notificationIds.length} connection{notificationIds.length > 1 && 's'} {notificationIds.length} {notificationIds.length > 1 ? translate('Notifications') : translate('Notification')}
</div> </div>
} }
{ {
!!restrictionIds.length && !!indexerProxyIds.length &&
<div> <div>
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} {indexerProxyIds.length} {indexerProxyIds.length > 1 ? translate('IndexerProxies') : translate('IndexerProxy')}
</div>
}
{
!!importListIds.length &&
<div>
{importListIds.length} list{importListIds.length > 1 && 's'}
</div> </div>
} }
</div> </div>
@@ -126,18 +108,16 @@ class Tag extends Component {
{ {
!isTagUsed && !isTagUsed &&
<div> <div>
No links {translate('NoLinks')}
</div> </div>
} }
<TagDetailsModal <TagDetailsModal
label={label} label={label}
isTagUsed={isTagUsed} isTagUsed={isTagUsed}
movieIds={movieIds} indexerIds={indexerIds}
delayProfileIds={delayProfileIds}
notificationIds={notificationIds} notificationIds={notificationIds}
restrictionIds={restrictionIds} indexerProxyIds={indexerProxyIds}
importListIds={importListIds}
isOpen={isDetailsModalOpen} isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress} onDeleteTagPress={this.onDeleteTagPress}
@@ -160,20 +140,16 @@ class Tag extends Component {
Tag.propTypes = { Tag.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired onConfirmDeleteTag: PropTypes.func.isRequired
}; };
Tag.defaultProps = { Tag.defaultProps = {
delayProfileIds: [], indexerIds: [],
notificationIds: [], notificationIds: [],
restrictionIds: [], indexerProxyIds: []
importListIds: [],
movieIds: []
}; };
export default Tag; export default Tag;
+8 -4
View File
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchNotifications } from 'Store/Actions/settingsActions'; import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions'; import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags'; import Tags from './Tags';
@@ -26,7 +26,8 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchTagDetails: fetchTagDetails, dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchNotifications: fetchNotifications dispatchFetchNotifications: fetchNotifications,
dispatchFetchIndexerProxies: fetchIndexerProxies
}; };
class MetadatasConnector extends Component { class MetadatasConnector extends Component {
@@ -37,11 +38,13 @@ class MetadatasConnector extends Component {
componentDidMount() { componentDidMount() {
const { const {
dispatchFetchTagDetails, dispatchFetchTagDetails,
dispatchFetchNotifications dispatchFetchNotifications,
dispatchFetchIndexerProxies
} = this.props; } = this.props;
dispatchFetchTagDetails(); dispatchFetchTagDetails();
dispatchFetchNotifications(); dispatchFetchNotifications();
dispatchFetchIndexerProxies();
} }
// //
@@ -58,7 +61,8 @@ class MetadatasConnector extends Component {
MetadatasConnector.propTypes = { MetadatasConnector.propTypes = {
dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchIndexerProxies: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
@@ -10,7 +10,7 @@ import {
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
const blacklistedProperties = [ const omittedProperties = [
'section', 'section',
'id' 'id'
]; ];
@@ -31,7 +31,7 @@ export default function createHandleActions(handlers, defaultState, section) {
if (section === baseSection) { if (section === baseSection) {
const newState = Object.assign(getSectionState(state, payloadSection), const newState = Object.assign(getSectionState(state, payloadSection),
_.omit(payload, blacklistedProperties)); _.omit(payload, omittedProperties));
return updateSectionState(state, payloadSection, newState); return updateSectionState(state, payloadSection, newState);
} }
@@ -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;
});
}
}
};
+2 -1
View File
@@ -202,7 +202,8 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'releases.customFilters', 'releases.customFilters',
'releases.selectedFilterKey' 'releases.selectedFilterKey',
'releases.columns'
]; ];
// //
@@ -7,6 +7,7 @@ import development from './Settings/development';
import downloadClients from './Settings/downloadClients'; import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories'; import indexerCategories from './Settings/indexerCategories';
import indexerProxies from './Settings/indexerProxies';
import languages from './Settings/languages'; import languages from './Settings/languages';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
import ui from './Settings/ui'; import ui from './Settings/ui';
@@ -14,6 +15,7 @@ import ui from './Settings/ui';
export * from './Settings/downloadClients'; export * from './Settings/downloadClients';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/indexerCategories'; export * from './Settings/indexerCategories';
export * from './Settings/indexerProxies';
export * from './Settings/languages'; export * from './Settings/languages';
export * from './Settings/notifications'; export * from './Settings/notifications';
export * from './Settings/applications'; export * from './Settings/applications';
@@ -35,6 +37,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState, downloadClients: downloadClients.defaultState,
general: general.defaultState, general: general.defaultState,
indexerCategories: indexerCategories.defaultState, indexerCategories: indexerCategories.defaultState,
indexerProxies: indexerProxies.defaultState,
languages: languages.defaultState, languages: languages.defaultState,
notifications: notifications.defaultState, notifications: notifications.defaultState,
applications: applications.defaultState, applications: applications.defaultState,
@@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers, ...downloadClients.actionHandlers,
...general.actionHandlers, ...general.actionHandlers,
...indexerCategories.actionHandlers, ...indexerCategories.actionHandlers,
...indexerProxies.actionHandlers,
...languages.actionHandlers, ...languages.actionHandlers,
...notifications.actionHandlers, ...notifications.actionHandlers,
...applications.actionHandlers, ...applications.actionHandlers,
@@ -84,6 +88,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers, ...downloadClients.reducers,
...general.reducers, ...general.reducers,
...indexerCategories.reducers, ...indexerCategories.reducers,
...indexerProxies.reducers,
...languages.reducers, ...languages.reducers,
...notifications.reducers, ...notifications.reducers,
...applications.reducers, ...applications.reducers,
@@ -43,6 +43,7 @@ function getInternalLink(source) {
function getTestLink(source, props) { function getTestLink(source, props) {
switch (source) { switch (source) {
case 'IndexerStatusCheck': case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return ( return (
<SpinnerIconButton <SpinnerIconButton
name={icons.TEST} name={icons.TEST}
+1 -14
View File
@@ -7,18 +7,6 @@ function isRelative(ajaxOptions) {
return !absUrlRegex.test(ajaxOptions.url); return !absUrlRegex.test(ajaxOptions.url);
} }
function moveBodyToQuery(ajaxOptions) {
if (ajaxOptions.data && ajaxOptions.type === 'DELETE') {
if (ajaxOptions.url.contains('?')) {
ajaxOptions.url += '&';
} else {
ajaxOptions.url += '?';
}
ajaxOptions.url += $.param(ajaxOptions.data);
delete ajaxOptions.data;
}
}
function addRootUrl(ajaxOptions) { function addRootUrl(ajaxOptions) {
ajaxOptions.url = apiRoot + ajaxOptions.url; ajaxOptions.url = apiRoot + ajaxOptions.url;
} }
@@ -32,7 +20,7 @@ function addContentType(ajaxOptions) {
if ( if (
ajaxOptions.contentType == null && ajaxOptions.contentType == null &&
ajaxOptions.dataType === 'json' && ajaxOptions.dataType === 'json' &&
(ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) { (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST' || ajaxOptions.method === 'DELETE')) {
ajaxOptions.contentType = 'application/json'; ajaxOptions.contentType = 'application/json';
} }
} }
@@ -52,7 +40,6 @@ export default function createAjaxRequest(originalAjaxOptions) {
const ajaxOptions = { dataType: 'json', ...originalAjaxOptions }; const ajaxOptions = { dataType: 'json', ...originalAjaxOptions };
if (isRelative(ajaxOptions)) { if (isRelative(ajaxOptions)) {
moveBodyToQuery(ajaxOptions);
addRootUrl(ajaxOptions); addRootUrl(ajaxOptions);
addApiKey(ajaxOptions); addApiKey(ajaxOptions);
addContentType(ajaxOptions); addContentType(ajaxOptions);
+8 -8
View File
@@ -30,9 +30,9 @@
"@fortawesome/free-regular-svg-icons": "5.15.3", "@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14", "@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "5.0.6", "@microsoft/signalr": "5.0.9",
"@sentry/browser": "6.3.1", "@sentry/browser": "6.10.0",
"@sentry/integrations": "6.3.1", "@sentry/integrations": "6.10.0",
"chart.js": "3.2.0", "chart.js": "3.2.0",
"classnames": "2.3.1", "classnames": "2.3.1",
"clipboard": "2.0.8", "clipboard": "2.0.8",
@@ -98,12 +98,12 @@
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.11.0", "core-js": "3.11.0",
"css-loader": "5.2.4", "css-loader": "5.2.4",
"eslint": "7.25.0", "eslint": "7.31.0",
"eslint-plugin-filenames": "1.3.2", "eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.22.1", "eslint-plugin-import": "2.23.4",
"eslint-plugin-react": "7.23.2", "eslint-plugin-react": "7.24.0",
"eslint-plugin-simple-import-sort": "7.0.0", "eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "2.0.0", "esprint": "3.1.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"filemanager-webpack-plugin": "5.0.0", "filemanager-webpack-plugin": "5.0.0",
"html-webpack-plugin": "5.3.1", "html-webpack-plugin": "5.3.1",
@@ -125,7 +125,7 @@
"webpack": "5.35.1", "webpack": "5.35.1",
"webpack-cli": "4.6.0", "webpack-cli": "4.6.0",
"webpack-livereload-plugin": "3.0.1", "webpack-livereload-plugin": "3.0.1",
"stylelint": "13.13.0", "stylelint": "13.13.1",
"stylelint-order": "4.1.0" "stylelint-order": "4.1.0"
} }
} }
+1 -1
View File
@@ -94,7 +94,7 @@
<!-- Standard testing packages --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
@@ -0,0 +1,24 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test.Http
{
[TestFixture]
public class UserAgentParserFixture : TestBase
{
// Ref *Arr `_userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({osName} {osVersion})";`
// Ref Mylar `Mylar3/' +str(hash) +'(' +vers +') +http://www.github.com/mylar3/mylar3/`
[TestCase("Mylar3/ 3ee23rh23irqfq (13123123) http://www.github.com/mylar3/mylar3/", "Mylar3")]
[TestCase("Lidarr/1.0.0.2300 (ubuntu 20.04)", "Lidarr")]
[TestCase("Radarr/1.0.0.2300 (ubuntu 20.04)", "Radarr")]
[TestCase("Readarr/1.0.0.2300 (ubuntu 20.04)", "Readarr")]
[TestCase("Sonarr/3.0.6.9999 (ubuntu 20.04)", "Sonarr")]
[TestCase("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "Other")]
public void should_parse_user_agent(string userAgent, string parsedAgent)
{
UserAgentParser.ParseSource(userAgent).Should().Be(parsedAgent);
}
}
}
@@ -19,6 +19,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")] [TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
[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://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(@"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")]
[TestCase(@"Req: [POST] https://www3.yggtorrent.nz/user/login: id=mySecret&pass=mySecret&ci_csrf_token=2b51db35e1912ffc138825a12b9933d2")]
// NzbGet // NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")] [TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
@@ -90,5 +94,22 @@ namespace NzbDrone.Common.Test.InstrumentationTests
cleansedMessage.Should().Be(message); cleansedMessage.Should().Be(message);
} }
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
public void should_not_clean_usetoken(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
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);
}
} }
} }
@@ -53,6 +53,16 @@ namespace NzbDrone.Common.Extensions
return dateTime >= afterDateTime && dateTime <= beforeDateTime; 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); public static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
} }
} }
@@ -194,5 +194,21 @@ namespace NzbDrone.Common.Extensions
var inputBytes = encoding.GetBytes(searchString); var inputBytes = encoding.GetBytes(searchString);
return encoding.GetString(WebUtility.UrlDecodeToBytes(inputBytes, 0, inputBytes.Length)); 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(' ');
}
} }
} }
@@ -59,7 +59,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
} }
webRequest.Proxy = GetProxy(request.Url); webRequest.Proxy = request.Proxy ?? GetProxy(request.Url);
if (request.Headers != null) if (request.Headers != null)
{ {
+1 -1
View File
@@ -61,7 +61,7 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient)); _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); var cookieContainer = InitializeRequestCookies(request);
+25 -2
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Text; using System.Text;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -30,6 +31,8 @@ namespace NzbDrone.Common.Http
public HttpUri Url { get; set; } public HttpUri Url { get; set; }
public HttpMethod Method { get; set; } public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; } public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; }
public IWebProxy Proxy { get; set; }
public byte[] ContentData { get; set; } public byte[] ContentData { get; set; }
public string ContentSummary { get; set; } public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
@@ -75,8 +78,28 @@ namespace NzbDrone.Common.Http
public void SetContent(string data) public void SetContent(string data)
{ {
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType); if (Encoding != null)
ContentData = encoding.GetBytes(data); {
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) public void AddBasicAuthentication(string username, string password)
+8 -1
View File
@@ -47,7 +47,14 @@ namespace NzbDrone.Common.Http
{ {
if (_content == null) 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; return _content;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
{ {
public static class UserAgentParser public static class UserAgentParser
{ {
private static readonly Regex AppSourceRegex = new Regex(@"(?<agent>.*)\/.*(\(.*\))?", private static readonly Regex AppSourceRegex = new Regex(@"(?<agent>[a-z0-9]*)\/.*(?:\(.*\))?",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static string SimplifyUserAgent(string userAgent) public static string SimplifyUserAgent(string userAgent)
@@ -11,13 +11,15 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[] private static readonly Regex[] CleansingRules = new[]
{ {
// Url // 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|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&| )[^=]*?(_?token|username|passwo?rd)=(?<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(@"rss\.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/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(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path // Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
+3 -3
View File
@@ -5,12 +5,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" /> <PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="DryIoc.dll" Version="4.7.6" /> <PackageReference Include="DryIoc.dll" Version="4.8.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.9" /> <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="SharpZipLib" Version="1.3.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
@@ -2,6 +2,8 @@ using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.IndexerProxies;
using NzbDrone.Core.IndexerProxies.Http;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -22,5 +24,30 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean(); Subject.Clean();
AllStoredModels.Should().BeEmpty(); AllStoredModels.Should().BeEmpty();
} }
[Test]
public void should_not_delete_used_tags()
{
var tags = Builder<Tag>
.CreateListOfSize(2)
.All()
.With(x => x.Id = 0)
.BuildList();
Db.InsertMany(tags);
var settings = Builder<HttpSettings>.CreateNew().Build();
var restrictions = Builder<IndexerProxyDefinition>.CreateListOfSize(2)
.All()
.With(x => x.Id = 0)
.With(x => x.Settings = settings)
.With(v => v.Tags.Add(tags[0].Id))
.BuildList();
Db.InsertMany(restrictions);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
} }
} }
@@ -33,9 +33,9 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{ {
var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json"); var recentFeed = ReadAllText(@"Files/Indexers/PrivateHD/recentfeed.json");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -32,9 +32,9 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
{ {
var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json"); var recentFeed = ReadAllText(@"Files/Indexers/FileList/recentfeed.json");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -19,7 +19,8 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Subject.Settings = new FileListSettings() Subject.Settings = new FileListSettings()
{ {
Passkey = "abcd", Passkey = "abcd",
Username = "somename" Username = "somename",
BaseUrl = "https://filelist.io"
}; };
Subject.Capabilities = new IndexerCapabilities Subject.Capabilities = new IndexerCapabilities
@@ -54,8 +55,6 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
SearchTerm = "Star Wars", SearchTerm = "Star Wars",
Categories = new int[] { 2000 } Categories = new int[] { 2000 }
}; };
Subject.BaseUrl = "https://filelist.io";
} }
private void MovieWithoutIMDB() private void MovieWithoutIMDB()
@@ -44,9 +44,9 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
{ {
var responseJson = ReadAllText(fileName); var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson)));
var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases; 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(); var responseJson = new { status = 5, message = "Invalid authentication credentials" }.ToJson();
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(v => v.ExecuteAsync(It.IsAny<HttpRequest>())) .Setup(o => o.ExecuteAsync(It.IsAny<HttpRequest>(), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson)))); .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; var torrents = (await Subject.Fetch(_movieSearchCriteria)).Releases;
@@ -1,10 +1,12 @@
using System; using System;
using System.Net; using System.Net;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -14,6 +16,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider> public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
{ {
private NewznabSettings _settings; private NewznabSettings _settings;
private IndexerDefinition _definition;
private string _caps; private string _caps;
[SetUp] [SetUp]
@@ -24,14 +27,24 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
BaseUrl = "http://indxer.local" 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"); _caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml");
} }
private void GivenCapsResponse(string caps) private void GivenCapsResponse(string caps)
{ {
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>())) .Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps)); .Returns<HttpRequest, IndexerDefinition>((r, d) => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
} }
[Test] [Test]
@@ -39,11 +52,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
GivenCapsResponse(_caps); GivenCapsResponse(_caps);
Subject.GetCapabilities(_settings); Subject.GetCapabilities(_settings, _definition);
Subject.GetCapabilities(_settings); Subject.GetCapabilities(_settings, _definition);
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Verify(o => o.Get(It.IsAny<HttpRequest>()), Times.Once()); .Verify(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()), Times.Once());
} }
[Test] [Test]
@@ -51,7 +64,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
GivenCapsResponse(_caps); GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings); var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(25); caps.LimitsDefault.Value.Should().Be(25);
caps.LimitsMax.Value.Should().Be(60); caps.LimitsMax.Value.Should().Be(60);
@@ -62,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
GivenCapsResponse(_caps.Replace("<limits", "<abclimits")); GivenCapsResponse(_caps.Replace("<limits", "<abclimits"));
var caps = Subject.GetCapabilities(_settings); var caps = Subject.GetCapabilities(_settings, _definition);
caps.LimitsDefault.Value.Should().Be(100); caps.LimitsDefault.Value.Should().Be(100);
caps.LimitsMax.Value.Should().Be(100); caps.LimitsMax.Value.Should().Be(100);
@@ -71,11 +84,11 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[Test] [Test]
public void should_throw_if_failed_to_get() public void should_throw_if_failed_to_get()
{ {
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>())) .Setup(o => o.Execute(It.IsAny<HttpRequest>(), It.IsAny<IndexerDefinition>()))
.Throws<Exception>(); .Throws<Exception>();
Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings)); Assert.Throws<Exception>(() => Subject.GetCapabilities(_settings, _definition));
} }
[Test] [Test]
@@ -83,7 +96,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
GivenCapsResponse(_caps.Replace("<limits", "<>")); GivenCapsResponse(_caps.Replace("<limits", "<>"));
Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings)); Assert.Throws<XmlException>(() => Subject.GetCapabilities(_settings, _definition));
} }
[Test] [Test]
@@ -91,7 +104,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
GivenCapsResponse(_caps.Replace("5030", "asdf")); GivenCapsResponse(_caps.Replace("5030", "asdf"));
var result = Subject.GetCapabilities(_settings); var result = Subject.GetCapabilities(_settings, _definition);
result.Should().NotBeNull(); result.Should().NotBeNull();
} }
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps = new IndexerCapabilities(); _caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps); .Returns(_caps);
} }
@@ -42,9 +42,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{ {
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml"); var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 }, Limit = 100, Offset = 0 })).Releases;
@@ -13,6 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator> public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
{ {
private MovieSearchCriteria _movieSearchCriteria; private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchCriteria;
private IndexerCapabilities _capabilities; private IndexerCapabilities _capabilities;
[SetUp] [SetUp]
@@ -30,10 +31,17 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
Categories = new int[] { 2000 } Categories = new int[] { 2000 }
}; };
_tvSearchCriteria = new TvSearchCriteria
{
SearchTerm = "Star Wars",
Categories = new int[] { 5000 },
Season = 0
};
_capabilities = new IndexerCapabilities(); _capabilities = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_capabilities); .Returns(_capabilities);
} }
@@ -178,5 +186,18 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
pageTier2.Url.Query.Should().NotContain("imdbid=0076759"); pageTier2.Url.Query.Should().NotContain("imdbid=0076759");
pageTier2.Url.Query.Should().Contain("q="); 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");
}
} }
} }
@@ -36,13 +36,13 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
Json.Serialize(authResponse, authStream); Json.Serialize(authResponse, authStream);
var responseJson = ReadAllText(fileName); var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString()))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString())));
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson))); .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; var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
@@ -37,9 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{ {
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json"); var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -64,9 +64,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test] [Test]
public async Task should_parse_error_20_as_empty_results() public async Task should_parse_error_20_as_empty_results()
{ {
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }"))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -76,9 +76,9 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[Test] [Test]
public async Task should_warn_on_unknown_error() public async Task should_warn_on_unknown_error()
{ {
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }"))); .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; var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Test.IndexerTests
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings> public class TestIndexer : UsenetIndexerBase<TestIndexerSettings>
{ {
public override string Name => "Test Indexer"; 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; public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
@@ -19,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public int _supportedPageSize; public int _supportedPageSize;
public override int PageSize => _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) : base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{ {
} }
@@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Generic;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Test.IndexerTests namespace NzbDrone.Core.Test.IndexerTests
{ {
public class TestIndexerSettings : IProviderConfig public class TestIndexerSettings : IIndexerSettings
{ {
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
@@ -14,5 +12,6 @@ namespace NzbDrone.Core.Test.IndexerTests
} }
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
} }
} }
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
_caps = new IndexerCapabilities(); _caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>() Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>())) .Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps); .Returns(_caps);
} }
@@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
{ {
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml"); var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; 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"); var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; 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"); var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET))) .Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET), Subject.Definition))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed))); .Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases; var releases = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
@@ -3,10 +3,10 @@
<TargetFrameworks>net5.0</TargetFrameworks> <TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.0.78" /> <PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
@@ -6,5 +6,6 @@ namespace NzbDrone.Core.Annotations
public string Name { get; set; } public string Name { get; set; }
public int Order { get; set; } public int Order { get; set; }
public string Hint { get; set; } public string Hint { get; set; }
public int? ParentValue { get; set; }
} }
} }
@@ -7,5 +7,6 @@ namespace NzbDrone.Core.Applications
public int IndexerId { get; set; } public int IndexerId { get; set; }
public int AppId { get; set; } public int AppId { get; set; }
public int RemoteIndexerId { get; set; } public int RemoteIndexerId { get; set; }
public string RemoteIndexerName { get; set; }
} }
} }
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Applications
{ {
List<AppIndexerMap> GetMappingsForApp(int appId); List<AppIndexerMap> GetMappingsForApp(int appId);
AppIndexerMap Insert(AppIndexerMap appIndexerMap); AppIndexerMap Insert(AppIndexerMap appIndexerMap);
AppIndexerMap Update(AppIndexerMap appIndexerMap);
void Delete(int mappingId); void Delete(int mappingId);
void DeleteAllForApp(int appId); void DeleteAllForApp(int appId);
} }
@@ -41,6 +42,11 @@ namespace NzbDrone.Core.Applications
return _appIndexerMapRepository.Insert(appIndexerMap); return _appIndexerMapRepository.Insert(appIndexerMap);
} }
public AppIndexerMap Update(AppIndexerMap appIndexerMap)
{
return _appIndexerMapRepository.Update(appIndexerMap);
}
public void Handle(ProviderDeletedEvent<IApplication> message) public void Handle(ProviderDeletedEvent<IApplication> message)
{ {
_appIndexerMapRepository.DeleteAllForApp(message.ProviderId); _appIndexerMapRepository.DeleteAllForApp(message.ProviderId);
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Applications
protected readonly IAppIndexerMapService _appIndexerMapService; protected readonly IAppIndexerMapService _appIndexerMapService;
protected readonly Logger _logger; 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); RegexOptions.IgnoreCase | RegexOptions.Compiled);
public abstract string Name { get; } public abstract string Name { get; }
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Applications
public abstract void AddIndexer(IndexerDefinition indexer); public abstract void AddIndexer(IndexerDefinition indexer);
public abstract void UpdateIndexer(IndexerDefinition indexer); public abstract void UpdateIndexer(IndexerDefinition indexer);
public abstract void RemoveIndexer(int indexerId); 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) public virtual object RequestAction(string action, IDictionary<string, string> query)
{ {
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@@ -110,18 +111,27 @@ namespace NzbDrone.Core.Applications
{ {
var indexerMappings = _appIndexerMapService.GetMappingsForApp(app.Definition.Id); 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 //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 //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) 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); _appIndexerMapService.Insert(addMapping);
indexerMappings.Add(addMapping); indexerMappings.Add(addMapping);
} }
@@ -214,5 +224,64 @@ namespace NzbDrone.Core.Applications
_logger.Error(ex, "An error occurred while talking to remote application."); _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);
}
} }
} }
@@ -9,6 +9,6 @@ namespace NzbDrone.Core.Applications
void AddIndexer(IndexerDefinition indexer); void AddIndexer(IndexerDefinition indexer);
void UpdateIndexer(IndexerDefinition indexer); void UpdateIndexer(IndexerDefinition indexer);
void RemoveIndexer(int indexerId); void RemoveIndexer(int indexerId);
Dictionary<int, int> GetIndexerMappings(); List<AppIndexerMap> GetIndexerMappings();
} }
} }
@@ -55,12 +55,12 @@ namespace NzbDrone.Core.Applications.Lidarr
return new ValidationResult(failures); return new ValidationResult(failures);
} }
public override Dictionary<int, int> GetIndexerMappings() public override List<AppIndexerMap> GetIndexerMappings()
{ {
var indexers = _lidarrV1Proxy.GetIndexers(Settings) var indexers = _lidarrV1Proxy.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) 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)) 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 //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 });
} }
} }
} }
@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Lidarr namespace NzbDrone.Core.Applications.Lidarr
@@ -26,8 +27,6 @@ namespace NzbDrone.Core.Applications.Lidarr
SyncCategories = new[] { 3000, 3010, 3030, 3040, 3050, 3060 }; SyncCategories = new[] { 3000, 3010, 3030, 3040, 3050, 3060 };
} }
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")] [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; } public string ProwlarrUrl { get; set; }
@@ -37,6 +36,9 @@ namespace NzbDrone.Core.Applications.Lidarr
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")] [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")]
public string ApiKey { get; set; } public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> SyncCategories { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -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 mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_mylarV3Proxy = mylarV3Proxy;
_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 mylarIndexer = BuildMylarIndexer(indexer, indexer.Protocol);
var remoteIndexer = _mylarV3Proxy.AddIndexer(mylarIndexer, 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 mylarIndexer = 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 mylarIndexer;
}
}
}
@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.Mylar
{
public class MylarError
{
public int Code { get; set; }
public string Message { get; set; }
}
}
@@ -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)
{
}
}
}
@@ -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;
}
}
}

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