Compare commits

...

155 Commits

Author SHA1 Message Date
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
Qstick
c6b6daaf80 Fixed: (Cardigann) DoLogin if search returns HttpError
Fixes #271
2021-06-23 22:05:28 -04:00
bakerboy448
3b42b6a7e0 Fixed: (BHD) Treat Limited Freeleech as Freeleech 2021-06-23 19:05:48 -05:00
Qstick
b6238f469c Fixed: (Orpheus) Use default Gazelle download links 2021-06-23 12:13:29 -04:00
Qstick
ae00c3aa6b Fixed: Cardigann redirect message 2021-06-23 12:13:29 -04:00
bakerboy448
8bf9d1b016 fix port [skip ci] 2021-06-23 01:55:57 -05:00
Qstick
19ed7aa804 Fixed: QueryString parsing for Cardigann failing 2021-06-22 21:12:04 -04:00
Qstick
4d129ada95 Fixed: (Gazelle) Not using freeleech token correctly
Fixes #258
2021-06-22 21:04:23 -04:00
bakerboy448
4ec8ea0e4d fixup bug report to note hidden text [skip ci] 2021-06-22 17:25:52 -05:00
bakerboy448
bd79d3c828 Fixed: Cardigann Indexer Descriptions & Indexer InfoLinks 2021-06-21 23:47:31 -05:00
Qstick
7fb6c539d4 Fixed: Require PID for AvistaZ indexers 2021-06-21 23:18:53 -04:00
Qstick
7f6fa2efbe Fixed: Newznab trying to test caps when disabled
Fixes #226
2021-06-21 23:07:05 -04:00
Qstick
b1727d9d91 Fixed: Set Download/Upload factors for Rarbg
Fixes #234
2021-06-21 23:07:05 -04:00
Qstick
3435d9db6e New: Allow users to use custom ymls 2021-06-21 22:45:09 -04:00
Qstick
8e597c8179 Fixed: Allow redirect and set referer for Anthelion 2021-06-21 21:23:38 -04:00
Qstick
80ec66514e change wiki links from wikjs to wiki endpoint 2021-06-21 21:13:08 -04:00
Weblate
88e677d973 Translated using Weblate (German)
Currently translated at 92.4% (393 of 425 strings)

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

Currently translated at 65.6% (279 of 425 strings)

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

Currently translated at 65.6% (279 of 425 strings)

Translated using Weblate (French)

Currently translated at 100.0% (425 of 425 strings)

Added translation using Weblate (Chinese (Simplified))

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: foXaCe <foxace66@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: vinson512 <vinson512@hotmail.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/zh_CN/
Translation: Servarr/Prowlarr
2021-06-21 19:56:22 -04:00
bakerboy448
a00f32c508 read me updates [skip ci] 2021-06-21 19:55:46 -04:00
bakerboy448
538db52d16 Fixed: Correct Password and Username field Privacy
Fixed: Consistent help text and settings for Credential Fields
2021-06-21 11:57:55 -05:00
Qstick
1ce7b0e56e New: (Indexer) Newznab preset for NZBNDX api 2021-06-20 17:47:42 -04:00
Yukine
87d91a0f15 Fixed: Support both old and new UNIT3D Id params (#244) 2021-06-19 19:47:08 -04:00
Qstick
61c1e934a5 New: Handle Freeleech flag at IndexerBase
This sets freeleech flag automatically for torrent releases with DownloadVolumeFactor = 0
2021-06-19 17:21:24 -04:00
Qstick
97b09335df fixup Anthelion category parse 2021-06-19 16:42:07 -04:00
Qstick
5e34fd2a9f New: (Indexer) Anthelion 2021-06-19 16:10:20 -04:00
Qstick
7e620bd156 New: (Indexer) ShowRss 2021-06-19 15:56:31 -04:00
Qstick
a97b801b24 New: (Indexer) GazelleGames 2021-06-19 15:48:28 -04:00
bakerboy448
9e64acd407 fix supported wikijs link 2021-06-19 10:36:33 -05:00
Yukine
f72269f91b fix(SpeedApp): correct status code check 2021-06-18 20:00:28 -04:00
Yukine
94d7f768a1 feat(Indexer): add SpeedApp C# indexer 2021-06-18 20:00:28 -04:00
bakerboy448
78cdc78cf9 indexer request method changes [skip ci] 2021-06-18 08:31:15 -05:00
Weblate
2fc1257f42 Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 51.5% (219 of 425 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (416 of 425 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (416 of 425 strings)

Translated using Weblate (Portuguese)

Currently translated at 82.3% (350 of 425 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (425 of 425 strings)

Translated using Weblate (French)

Currently translated at 98.3% (418 of 425 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: flowcool <a121313b@opayq.com>
Co-authored-by: vinson512 <vinson512@hotmail.com>
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/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2021-06-17 22:09:01 -04:00
bakerboy448
210311cb38 last fixup... [skip ci] 2021-06-16 13:49:12 -05:00
bakerboy448
9d7ec89314 fixups... [skip ci] 2021-06-16 13:48:25 -05:00
bakerboy448
9c279701a6 update indexer template - no links [skip ci] 2021-06-16 13:46:41 -05:00
bakerboy448
743e2e9b21 Update indexer_request.md
add link to GHI
2021-06-16 12:55:14 -05:00
bakerboy448
4a8daea940 Update indexer_request.md
add link
2021-06-16 12:53:46 -05:00
Qstick
bd90b74c12 Attempt to fix ZonaQ Auth 2021-06-16 00:58:28 -04:00
Qstick
4dff0c075a Log cleanse for passwrd form param 2021-06-16 00:43:54 -04:00
Qstick
b8f57507dd Update README.md 2021-06-16 00:33:24 -04:00
Qstick
61bfa9e7ed New: Better Application Tests 2021-06-16 00:20:42 -04:00
Qstick
bbea256c85 Fixed: Dynamic page size for Cardigann search path paging
Fixes some indexers that have arbitrary paging paths in their search-paths definitions. Set page size based on first request, break out if a request down the line returns less results.
2021-06-15 23:53:52 -04:00
Qstick
5a1186639e Fixed: Cookie set for Cardigann cookie indexers 2021-06-15 23:51:33 -04:00
TheCatLady
a8f2700fe6 Sort average response times 2021-06-15 21:14:11 -04:00
Yukine
eeec505182 Fixed: Remove Camel Case DB Converter for Cookies (#223)
* fix(ProviderRepository): remove DictionaryKeyPolicy CamelCase Naming

* Revert "fix(ProviderRepository): remove DictionaryKeyPolicy CamelCase Naming"

This reverts commit 90c95b240f.

* fix(TableMapping): make own non Camel Case Converter for Cookies
2021-06-15 21:13:37 -04:00
bakerboy448
66dc53b92f Fixed: Remove Defunct Indexer - Usenet - NZBs2GO (#216)
Fixes #214
2021-06-14 15:12:57 -05:00
Qstick
334f3514df Cleanup conversion script issues 2021-06-13 18:26:32 -04:00
Qstick
6ce35f6a24 Fix Nebulance Settings copy paste issues 2021-06-13 17:49:15 -04:00
Qstick
f6a5f887ce New: Convert Shareisland to use API 2021-06-13 01:40:07 -04:00
Qstick
bc90415394 New: (Indexer) Nebulance 2021-06-13 01:10:46 -04:00
237 changed files with 12407 additions and 1390 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# 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
open_collective: prowlarr
ko_fi: # Replace with a single Ko-fi username

View File

@@ -1,36 +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! -->
**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-->

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

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

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Indexer Requests
url: https://requests.prowlarr.com/
about: Request new indexers to be added. Vote on existing requests.
- name: Support via Discord
url: https://prowlarr.com/discord
about: Chat with users and devs on support and setup related topics.

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
---
name: Indexer Request
about: Request an indexer for Prowlarr. Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests will be closed without warning. Please search GitHub prior to requesting.
title: '(Indexer) '
labels: 'Type: Indexer Request'
assignees: ''
---
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
**Type:** <Usenet|Torrents>
**Tracker:** <Indexer/Tracker Name>
**URL:** <Indexer/Tracker URL>
**In Jackett?:** <Yes|No>
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
**Additional Context:**
<!-- Add any other context or screenshots about the request here. -->

View File

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

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

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

View File

@@ -2,8 +2,10 @@
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/prowlarr/contributing).
## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.servarr.com/prowlarr) the better.
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better.
## Development ##
@@ -23,11 +25,11 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.ser
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Prowlarr.Console` and framework to `net5.0`
6. Debug the project in Visual Studio
7. Open http://localhost:7878
7. Open http://localhost:9696
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Radarr's develop branch, don't merge
- Rebase from Prowlarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the discord if you have any questions

View File

@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wikijs.servarr.com/prowlarr/installation#docker)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors)
@@ -10,12 +10,14 @@
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).
## Major Features Include:
- Usenet support for any Newznab compatible indexer, including Headphones VIP
- Torrent support 400+ trackers & more coming soon
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
- Torrent support for over 500 trackers with more added all the time
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required
- Indexer History and Statistics
- Manual Searching of Trackers & Indexers at a category level
- Support for pushing releases directly to your download clients from Prowlarr
- Indexer health and status notifications
## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
@@ -23,11 +25,14 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Prowlarr/Prowlarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wikijs.servarr.com/prowlarr)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
## Feature Requests
## Indexers/Trackers
[Feature Requests](https://github.com/Prowlarr/Prowlarr/issues/new?assignees=&template=feature_request.md&Type%3A%20Feature%20Request&title=)
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
[Indexer Requests](https://requests.prowlarr.com)
- Request or vote on an existing request for a new tracker/indexer
## Contributors & Developers
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).

View File

@@ -13,7 +13,7 @@ variables:
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.203'
dotnetVersion: '5.0.302'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
@@ -879,7 +879,7 @@ stages:
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none
- powershell: |
- pwsh: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

View File

@@ -129,7 +129,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
<Link to="https://wikijs.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
<Link to="https://wiki.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
{translate('MappedDrivesRunningAsService')}
</Link>
</Alert>

View File

@@ -13,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
@@ -31,6 +32,7 @@ function EditIndexerModalContent(props) {
onSavePress,
onTestPress,
onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps
} = props;
@@ -165,6 +167,12 @@ function EditIndexerModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -204,6 +212,7 @@ EditIndexerModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,10 @@ function getAverageResponseTimeData(indexerStats) {
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import TextInput from 'Components/Form/TextInput';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import translate from 'Utilities/String/translate';
import SearchFooterLabel from './SearchFooterLabel';
import styles from './SearchFooter.css';
@@ -167,7 +168,7 @@ class SearchFooter extends Component {
isDisabled={isFetching || !hasIndexers}
onPress={this.onSearchPress}
>
Search
{translate('Search')}
</SpinnerButton>
</div>
</div>

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ function UpdateSettings(props) {
type={inputTypes.TEXT}
name="branch"
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
{...branch}
onChange={onInputChange}
readOnly={usingExternalUpdateMechanism}
@@ -92,7 +92,7 @@ function UpdateSettings(props) {
name="updateMechanism"
values={updateOptions}
helpText={translate('UpdateMechanismHelpText')}
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
onChange={onInputChange}
{...updateMechanism}
/>

View File

@@ -26,7 +26,7 @@ function NotificationEventItems(props) {
<div>
<FormInputHelpText
text={translate('NotificationTriggersHelpText')}
link="https://wikijs.servarr.com/prowlarr/settings#connections"
link="https://wiki.servarr.com/prowlarr/settings#connections"
/>
<div className={styles.events}>
<div>

View File

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

View File

@@ -13,7 +13,7 @@ function createHealthCheckSelector() {
source: 'UI',
type: 'warning',
message: translate('CouldNotConnectSignalR'),
wikiUrl: 'https://wikijs.servarr.com/prowlarr/system#could-not-connect-to-signalr'
wikiUrl: 'https://wiki.servarr.com/prowlarr/system#could-not-connect-to-signalr'
});
}

View File

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

View File

@@ -22,7 +22,7 @@ class MoreInfo extends Component {
<DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://wikijs.servarr.com/prowlarr">wikijs.servarr.com/prowlarr</Link>
<Link to="https://wiki.servarr.com/prowlarr">wiki.servarr.com/prowlarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>{translate('Reddit')}</DescriptionListItemTitle>

View File

@@ -252,7 +252,7 @@
</span>
<a
href="https://wikijs.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
href="https://wiki.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
class="forgot-password"
>Forgot your password?</a
>

View File

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

View File

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

View File

@@ -19,6 +19,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[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://torrentdb.net/login: _token=2b51db35e1912ffc138825a12b9933d2&username=mySecret&password=mySecret&remember=on")]
[TestCase(@" var authkey = ""2b51db35e1910123321025a12b9933d2"";")]
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
@@ -90,5 +93,14 @@ namespace NzbDrone.Common.Test.InstrumentationTests
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);
}
}
}

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ namespace NzbDrone.Common.Http
public HttpUri Url { get; set; }
public HttpMethod Method { get; set; }
public HttpHeader Headers { get; set; }
public Encoding Encoding { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; }
@@ -75,8 +76,15 @@ namespace NzbDrone.Common.Http
public void SetContent(string data)
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
if (Encoding != null)
{
ContentData = Encoding.GetBytes(data);
}
else
{
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
}
}
public void AddBasicAuthentication(string username, string password)

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Test.HealthCheck
[TestFixture]
public class HealthCheckFixture : CoreTest
{
private const string WikiRoot = "https://wikijs.servarr.com/";
private const string WikiRoot = "https://wiki.servarr.com/";
[TestCase("I blew up because of some weird user mistake", null, WikiRoot + "prowlarr/system#i-blew-up-because-of-some-weird-user-mistake")]
[TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "prowlarr/system#my-health-check")]

View File

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

View File

@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Test.IndexerTests
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings>
{
public override string Name => "Test Indexer";
public override string BaseUrl => "http://testindexer.com";
public override string[] IndexerUrls => new string[] { "http://testindexer.com" };
public override string Description => "";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Applications
protected readonly IAppIndexerMapService _appIndexerMapService;
protected readonly Logger _logger;
protected static readonly Regex AppIndexerRegex = new Regex(@"(?<indexer>\d*)/api",
protected static readonly Regex AppIndexerRegex = new Regex(@"\/(?<indexer>\d.)\/",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public abstract string Name { get; }

View File

@@ -114,7 +114,12 @@ namespace NzbDrone.Core.Applications
var prowlarrMappings = indexerMappings.ToDictionary(i => i.RemoteIndexerId, i => i.IndexerId);
//Get Dictionary of Remote Indexers point to Prowlarr and what they are mapped to
var remoteMappings = app.GetIndexerMappings();
var remoteMappings = ExecuteAction(a => a.GetIndexerMappings(), app);
if (remoteMappings == null)
{
continue;
}
//Add mappings if not already in db, these were setup manually in the app or orphaned by a table wipe
foreach (var mapping in remoteMappings)
@@ -214,5 +219,64 @@ namespace NzbDrone.Core.Applications
_logger.Error(ex, "An error occurred while talking to remote application.");
}
}
private TResult ExecuteAction<TResult>(Func<IApplication, TResult> applicationAction, IApplication application)
{
TResult result;
try
{
result = applicationAction(application);
_applicationStatusService.RecordSuccess(application.Definition.Id);
return result;
}
catch (WebException webException)
{
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
webException.Status == WebExceptionStatus.ConnectFailure)
{
_applicationStatusService.RecordConnectionFailure(application.Definition.Id);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id);
}
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
}
else
{
_logger.Warn("{0} {1}", this, webException.Message);
}
}
catch (TooManyRequestsException ex)
{
if (ex.RetryAfter != TimeSpan.Zero)
{
_applicationStatusService.RecordFailure(application.Definition.Id, ex.RetryAfter);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id, TimeSpan.FromHours(1));
}
_logger.Warn("API Request Limit reached for {0}", this);
}
catch (HttpException ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Warn("{0} {1}", this, ex.Message);
}
catch (Exception ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Error(ex, "An error occurred while talking to remote application.");
}
return default(TResult);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Lidarr
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_lidarrV1Proxy.Test(Settings));
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
try
{
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), 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 Lidarr"));
}
return new ValidationResult(failures);
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s):// and port 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; }
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings);
void RemoveIndexer(int indexerId, LidarrSettings settings);
LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings);
ValidationFailure Test(LidarrSettings settings);
ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings);
}
public class LidarrV1Proxy : ILidarrV1Proxy
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Lidarr
return Execute<LidarrIndexer>(request);
}
public ValidationFailure Test(LidarrSettings settings)
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try
{
GetStatus(settings);
Execute<LidarrIndexer>(request);
}
catch (HttpException ex)
{
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Lidarr
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Radarr
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_radarrV3Proxy.Test(Settings));
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies);
try
{
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), 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 Radarr"));
}
return new ValidationResult(failures);
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Radarr
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s):// and port if needed")]
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")]

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings);
void RemoveIndexer(int indexerId, RadarrSettings settings);
RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings);
ValidationFailure Test(RadarrSettings settings);
ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings);
}
public class RadarrV3Proxy : IRadarrV3Proxy
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Radarr
return Execute<RadarrIndexer>(request);
}
public ValidationFailure Test(RadarrSettings settings)
public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try
{
GetStatus(settings);
Execute<RadarrIndexer>(request);
}
catch (HttpException ex)
{
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Radarr
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Readarr
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_readarrV1Proxy.Test(Settings));
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Books);
try
{
failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), 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 Readarr"));
}
return new ValidationResult(failures);
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Readarr
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s):// and port if needed")]
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Readarr
List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings);
void RemoveIndexer(int indexerId, ReadarrSettings settings);
ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings);
ValidationFailure Test(ReadarrSettings settings);
ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings);
}
public class ReadarrV1Proxy : IReadarrV1Proxy
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Readarr
return Execute<ReadarrIndexer>(request);
}
public ValidationFailure Test(ReadarrSettings settings)
public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try
{
GetStatus(settings);
Execute<ReadarrIndexer>(request);
}
catch (HttpException ex)
{
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Readarr
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Sonarr
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_sonarrV3Proxy.Test(Settings));
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
try
{
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), 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 Sonarr"));
}
return new ValidationResult(failures);
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Sonarr
public IEnumerable<int> SyncCategories { get; set; }
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s):// and port if needed")]
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Sonarr
List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings);
void RemoveIndexer(int indexerId, SonarrSettings settings);
SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings);
ValidationFailure Test(SonarrSettings settings);
ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings);
}
public class SonarrV3Proxy : ISonarrV3Proxy
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Sonarr
return Execute<SonarrIndexer>(request);
}
public ValidationFailure Test(SonarrSettings settings)
public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
request.SetContent(indexer.ToJson());
try
{
GetStatus(settings);
Execute<SonarrIndexer>(request);
}
catch (HttpException ex)
{
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Sonarr
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{

View File

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

View File

@@ -180,7 +180,7 @@ namespace NzbDrone.Core.Configuration
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
// TODO: Change back to "master" for the first stable release.
public string Branch => GetValue("Branch", "nightly").ToLowerInvariant();
public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
var releaseInfoPath = Path.Combine(bin, "release_info");
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
DefaultBranch = "nightly";
DefaultBranch = "develop";
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
{

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using Dapper;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Datastore.Converters
{
public class CookieConverter : SqlMapper.TypeHandler<IDictionary<string, string>>
{
protected readonly JsonSerializerOptions SerializerSettings;
public CookieConverter()
{
var serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
IgnoreNullValues = true,
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
SerializerSettings = serializerSettings;
}
public override void SetValue(IDbDataParameter parameter, IDictionary<string, string> value)
{
parameter.Value = JsonSerializer.Serialize(value, SerializerSettings);
}
public override IDictionary<string, string> Parse(object value)
{
return JsonSerializer.Deserialize<Dictionary<string, string>>((string)value, SerializerSettings);
}
}
}

View File

@@ -108,10 +108,10 @@ namespace NzbDrone.Core.Datastore
if (OsInfo.IsOsx)
{
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wikijs.servarr.com/prowlarr/faq#i-use-prowlarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-use-prowlarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
}
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wikijs.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
}
catch (Exception e)
{

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
using System.Data;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(8)]
public class redacted_api : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToRedactedApi);
}
private void MigrateToRedactedApi(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT Id, Settings FROM Indexers WHERE Implementation = 'Redacted'";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = Json.Deserialize<JObject>(settings);
// Remove username
if (jsonObject.ContainsKey("username"))
{
jsonObject.Remove("username");
}
// Remove password
if (jsonObject.ContainsKey("password"))
{
jsonObject.Remove("password");
}
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE Indexers SET Settings = ?, ConfigContract = ?, Enable = 0 WHERE Id = ?";
updateCmd.AddParameter(settings);
updateCmd.AddParameter("RedactedSettings");
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}
}

View File

@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.Description)
.Ignore(i => i.Language)
.Ignore(i => i.Encoding)
.Ignore(i => i.BaseUrl)
.Ignore(i => i.IndexerUrls)
.Ignore(i => i.Protocol)
.Ignore(i => i.Privacy)
.Ignore(i => i.SupportsRss)
@@ -100,7 +100,7 @@ namespace NzbDrone.Core.Datastore
SqlMapper.RemoveTypeMap(typeof(DateTime));
SqlMapper.AddTypeHandler(new DapperUtcConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<IDictionary<string, string>>());
SqlMapper.AddTypeHandler(new CookieConverter());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.Clients.Blackhole
{
public class TorrentBlackhole : TorrentClientBase<TorrentBlackholeSettings>
{
public override bool PreferTorrentFile => true;
public TorrentBlackhole(ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
{
}
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
{
throw new NotImplementedException("Blackhole does not support redirected indexers.");
}
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
{
if (!Settings.SaveMagnetFiles)
{
throw new NotSupportedException("Blackhole does not support magnet links.");
}
var title = release.Title;
title = title.CleanFileName();
var filepath = Path.Combine(Settings.TorrentFolder, $"{title}.{Settings.MagnetFileExtension.Trim('.')}");
var fileContent = Encoding.UTF8.GetBytes(magnetLink);
using (var stream = _diskProvider.OpenWriteStream(filepath))
{
stream.Write(fileContent, 0, fileContent.Length);
}
_logger.Debug("Saving magnet link succeeded, saved to: {0}", filepath);
return null;
}
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
{
var title = release.Title;
title = title.CleanFileName();
var filepath = Path.Combine(Settings.TorrentFolder, string.Format("{0}.torrent", title));
using (var stream = _diskProvider.OpenWriteStream(filepath))
{
stream.Write(fileContent, 0, fileContent.Length);
}
_logger.Debug("Torrent Download succeeded, saved to: {0}", filepath);
return null;
}
public override string Name => "Torrent Blackhole";
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder"));
}
}
}

View File

@@ -0,0 +1,46 @@
using System.ComponentModel;
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Core.Download.Clients.Blackhole
{
public class TorrentBlackholeSettingsValidator : AbstractValidator<TorrentBlackholeSettings>
{
public TorrentBlackholeSettingsValidator()
{
//Todo: Validate that the path actually exists
RuleFor(c => c.TorrentFolder).IsValidPath();
RuleFor(c => c.MagnetFileExtension).NotEmpty();
}
}
public class TorrentBlackholeSettings : IProviderConfig
{
public TorrentBlackholeSettings()
{
MagnetFileExtension = ".magnet";
}
private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator();
[FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .torrent file")]
public string TorrentFolder { get; set; }
[DefaultValue(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[FieldDefinition(1, Label = "Save Magnet Files", Type = FieldType.Checkbox, HelpText = "Save a .magnet file with the magnet link if no .torrent file is available (only useful if the download client supports .magnet files)")]
public bool SaveMagnetFiles { get; set; }
[FieldDefinition(2, Label = "Save Magnet Files", Type = FieldType.Textbox, HelpText = "Extension to use for magnet links, defaults to '.magnet'")]
public string MagnetFileExtension { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.IO;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.Clients.Blackhole
{
public class UsenetBlackhole : UsenetClientBase<UsenetBlackholeSettings>
{
public UsenetBlackhole(IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
Logger logger)
: base(httpClient, configService, diskProvider, logger)
{
}
protected override string AddFromLink(ReleaseInfo release)
{
throw new NotSupportedException("Blackhole does not support redirected indexers.");
}
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
{
var title = release.Title;
title = title.CleanFileName();
var filepath = Path.Combine(Settings.NzbFolder, title + ".nzb");
using (var stream = _diskProvider.OpenWriteStream(filepath))
{
stream.Write(fileContent, 0, fileContent.Length);
}
_logger.Debug("NZB Download succeeded, saved to: {0}", filepath);
return null;
}
public override string Name => "Usenet Blackhole";
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestFolder(Settings.NzbFolder, "NzbFolder"));
}
}
}

View File

@@ -0,0 +1,29 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Core.Download.Clients.Blackhole
{
public class UsenetBlackholeSettingsValidator : AbstractValidator<UsenetBlackholeSettings>
{
public UsenetBlackholeSettingsValidator()
{
RuleFor(c => c.NzbFolder).IsValidPath();
}
}
public class UsenetBlackholeSettings : IProviderConfig
{
private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator();
[FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .nzb file")]
public string NzbFolder { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -41,10 +41,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBGet url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
public string UrlBase { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]

View File

@@ -6,7 +6,6 @@ using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;

View File

@@ -1,5 +1,4 @@
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using MonoTorrent;

View File

@@ -1,6 +1,5 @@
using System;
using NzbDrone.Common.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Exceptions
{

View File

@@ -1,5 +1,4 @@
using System;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Exceptions
{

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck
private static HttpUri MakeWikiUrl(string fragment)
{
return new HttpUri("https://wikijs.servarr.com/prowlarr/system#") + new HttpUri(fragment);
return new HttpUri("https://wiki.servarr.com/prowlarr/system#") + new HttpUri(fragment);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.History
History MostRecentForIndexer(int indexerId);
List<History> Since(DateTime date, HistoryEventType? eventType);
void Cleanup(int days);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
}
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
@@ -87,5 +89,21 @@ namespace NzbDrone.Core.History
return Query(builder).OrderBy(h => h.Date).ToList();
}
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
{
var builder = new SqlBuilder()
.SelectCount()
.Where<History>(x => x.IndexerId == indexerId)
.Where<History>(x => x.Date >= date)
.Where<History>(x => eventTypes.Contains(x.EventType));
var sql = builder.AddPageCountTemplate(typeof(History));
using (var conn = _database.OpenConnection())
{
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters);
}
}
}
}

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Core.History
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
void UpdateMany(List<History> toUpdate);
List<History> Since(DateTime date, HistoryEventType? eventType);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
}
public class HistoryService : IHistoryService,
@@ -205,5 +206,10 @@ namespace NzbDrone.Core.History
{
_historyRepository.Purge(vacuum: true);
}
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
{
return _historyRepository.CountSince(indexerId, date, eventTypes);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.IndexerSearch
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
RegexOptions.Compiled);
public List<ReleaseInfo> Releases;
public List<ReleaseInfo> Releases { get; set; }
private static string RemoveInvalidXMLChars(string text)
{
@@ -100,7 +100,8 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("minimumratio", t.MinimumRatio, protocol),
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),
GetNabElement("downloadvolumefactor", t.DownloadVolumeFactor, protocol),
GetNabElement("uploadvolumefactor", t.UploadVolumeFactor, protocol)))));
GetNabElement("uploadvolumefactor", t.UploadVolumeFactor, protocol),
GetNabElement("coverurl", r.PosterUrl, protocol)))));
return xdoc.Declaration + Environment.NewLine + xdoc;
}

View File

@@ -4,7 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -20,19 +19,19 @@ namespace NzbDrone.Core.IndexerSearch
public class NzbSearchService : ISearchForNzb
{
private readonly IIndexerLimitService _indexerLimitService;
private readonly IEventAggregator _eventAggregator;
private readonly IIndexerFactory _indexerFactory;
private readonly IDownloadMappingService _downloadMappingService;
private readonly Logger _logger;
public NzbSearchService(IEventAggregator eventAggregator,
IIndexerFactory indexerFactory,
IDownloadMappingService downloadMappingService,
IIndexerLimitService indexerLimitService,
Logger logger)
{
_eventAggregator = eventAggregator;
_indexerFactory = indexerFactory;
_downloadMappingService = downloadMappingService;
_indexerLimitService = indexerLimitService;
_logger = logger;
}
@@ -163,16 +162,36 @@ namespace NzbDrone.Core.IndexerSearch
private async Task<IList<ReleaseInfo>> DispatchIndexer(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, IIndexer indexer, SearchCriteriaBase criteriaBase)
{
if (_indexerLimitService.AtQueryLimit((IndexerDefinition)indexer.Definition))
{
return new List<ReleaseInfo>();
}
try
{
var indexerReports = await searchAction(indexer);
var releases = indexerReports.Releases;
//Filter results to only those in searched categories
if (criteriaBase.Categories.Length > 0)
{
var expandedQueryCats = ((IndexerDefinition)indexer.Definition).Capabilities.Categories.ExpandTorznabQueryCategories(criteriaBase.Categories);
releases = releases.Where(result => result.Categories?.Any() != true || expandedQueryCats.Intersect(result.Categories.Select(c => c.Id)).Any()).ToList();
if (releases.Count != indexerReports.Releases.Count)
{
_logger.Trace("{0} {1} Releases which didn't contain search categories [{2}] were filtered", indexerReports.Releases.Count - releases.Count, indexer.Name, string.Join(", ", expandedQueryCats));
}
}
foreach (var query in indexerReports.Queries)
{
_eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, query.ElapsedTime, query.StatusCode == 200, query.Releases.Count()));
}
return indexerReports.Releases;
return releases;
}
catch (Exception e)
{

View File

@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Messaging.Commands;
@@ -24,7 +25,17 @@ namespace NzbDrone.Core.IndexerVersions
public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, IExecute<IndexerDefinitionUpdateCommand>
{
private const int DEFINITION_VERSION = 1;
private readonly List<string> _defintionBlacklist = new List<string>() { "aither", "animeworld", "blutopia", "beyond-hd", "beyond-hd-oneurl", "hdbits" };
private readonly List<string> _defintionBlacklist = new List<string>()
{
"aither",
"animeworld",
"blutopia",
"beyond-hd",
"beyond-hd-oneurl",
"danishbytes",
"hdbits",
"shareisland"
};
private readonly IHttpClient _httpClient;
private readonly IAppFolderInfo _appFolderInfo;
@@ -59,6 +70,40 @@ namespace NzbDrone.Core.IndexerVersions
var request = new HttpRequest($"https://indexers.prowlarr.com/master/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlacklist.Contains(i.File)).ToList();
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions", "Custom");
var directoryInfo = new DirectoryInfo(definitionFolder);
if (directoryInfo.Exists)
{
var files = directoryInfo.GetFiles($"*.yml");
foreach (var file in files)
{
_logger.Debug("Loading Custom Cardigann definition " + file.FullName);
try
{
var definitionString = File.ReadAllText(file.FullName);
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
definition.File = Path.GetFileNameWithoutExtension(file.Name);
if (indexerList.Any(i => i.File == definition.File || i.Name == definition.Name))
{
_logger.Warn("Custom Cardigann definition {0} does not have unique file name or Indexer name", file.FullName);
continue;
}
indexerList.Add(definition);
}
catch (Exception e)
{
_logger.Error($"Error while parsing custom Cardigann definition {file.FullName}\n{e}");
}
}
}
}
catch
{
@@ -108,7 +153,7 @@ namespace NzbDrone.Core.IndexerVersions
if (directoryInfo.Exists)
{
var files = directoryInfo.GetFiles($"{fileKey}.yml");
var files = directoryInfo.GetFiles($"{fileKey}.yml", SearchOption.AllDirectories);
if (files.Any())
{

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class Aither : Unit3dBase
{
public override string Name => "Aither";
public override string BaseUrl => "https://aither.cc/";
public override string[] IndexerUrls => new string[] { "https://aither.cc/" };
public override string Description => "Aither is a Private Torrent Tracker for HD MOVIES / TV";
public override string Language => "en-us";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;

View File

@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AlphaRatio : Gazelle.Gazelle
{
public override string Name => "AlphaRatio";
public override string BaseUrl => "https://alpharatio.cc/";
public override string[] IndexerUrls => new string[] { "https://alpharatio.cc/" };
public override string Description => "AlphaRatio(AR) is a Private Torrent Tracker for 0DAY / GENERAL";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
@@ -25,8 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Settings = Settings,
HttpClient = _httpClient,
Logger = _logger,
Capabilities = Capabilities,
BaseUrl = BaseUrl
Capabilities = Capabilities
};
}
@@ -40,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Definitions
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
MovieSearchParam.Q
}
};

View File

@@ -0,0 +1,550 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Anidub : TorrentIndexerBase<AnidubSettings>
{
public override string Name => "Anidub";
public override string[] IndexerUrls => new string[] { "https://tr.anidub.com/" };
public override string Description => "Anidub is russian anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-ru";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPublic;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anidub(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnidubRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnidubParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
}
protected override async Task DoLogin()
{
UpdateCookies(null, null);
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php")
{
LogResponseContent = true,
AllowAutoRedirect = true
};
var mainPage = await _httpClient.ExecuteAsync(new HttpRequest(Settings.BaseUrl));
requestBuilder.Method = Common.Http.HttpMethod.POST;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
requestBuilder.SetCookies(mainPage.GetCookies());
var authLoginRequest = requestBuilder
.AddFormParameter("login_name", Settings.Username)
.AddFormParameter("login_password", Settings.Password)
.AddFormParameter("login", "submit")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && !CheckIfLoginNeeded(response))
{
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Anidub authentication succeeded");
}
else
{
const string ErrorSelector = "#content .berror .berror_c";
var parser = new HtmlParser();
var document = await parser.ParseDocumentAsync(response.Content);
var errorMessage = document.QuerySelector(ErrorSelector).TextContent.Trim();
throw new IndexerAuthException("Anidub authentication failed. Error: " + errorMessage);
}
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (httpResponse.Content.Contains("index.php?action=logout"))
{
return false;
}
return true;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Аниме TV");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Movies, "Аниме Фильмы");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVAnime, "Аниме OVA");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVAnime, "Аниме OVA |- Аниме ONA");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TV, "Дорамы / Японские Сериалы и Фильмы");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TV, "Дорамы / Корейские Сериалы и Фильмы");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TV, "Дорамы / Китайские Сериалы и Фильмы");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.TV, "Дорамы");
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVAnime, "Аниме TV / Аниме Ongoing");
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.TVAnime, "Аниме TV / Многосерийный сёнэн");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.Other, "Аниме Ongoing Анонсы");
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.XXX, "18+");
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TVAnime, "Аниме TV / Законченные сериалы");
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.BooksComics, "Манга");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.Audio, "OST");
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.Audio, "Подкасты");
return caps;
}
}
public class AnidubRequestGenerator : IIndexerRequestGenerator
{
public AnidubSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public AnidubRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var requestUrl = string.Empty;
var isSearch = !string.IsNullOrWhiteSpace(term);
if (isSearch)
{
requestUrl = string.Format("{0}/index.php?do=search", Settings.BaseUrl.TrimEnd('/'));
}
else
{
requestUrl = Settings.BaseUrl;
}
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
if (isSearch)
{
request.HttpRequest.Method = NzbDrone.Common.Http.HttpMethod.POST;
var postData = new NameValueCollection
{
{ "do", "search" },
{ "subaction", "search" },
{ "search_start", "1" },
{ "full_search", "1" },
{ "result_from", "1" },
// Remove season and episode info from search term cause it breaks search
{ "story", Regex.Replace(term, @"(?:[SsEe]?\d{1,4}){1,2}$", "").TrimEnd() },
{ "titleonly", "3" },
{ "searchuser", "" },
{ "replyless", "0" },
{ "replylimit", "0" },
{ "searchdate", "0" },
{ "beforeafter", "after" },
{ "sortby", "" },
{ "resorder", "desc" },
{ "showposts", "1" },
{ "catlist[]", "0" }
};
var headers = new NameValueCollection
{
{ "Content-Type", "application/x-www-form-urlencoded" }
};
request.HttpRequest.SetContent(postData.GetQueryString());
request.HttpRequest.Headers.Add(headers);
}
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnidubParser : IParseIndexerResponse
{
private readonly AnidubSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public IHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
private static Dictionary<string, string> CategoriesMap => new Dictionary<string, string>
{
{ "/anime_tv/full", "14" },
{ "/anime_tv/anime_ongoing", "10" },
{ "/anime_tv/shonen", "11" },
{ "/anime_tv", "2" },
{ "/xxx", "13" },
{ "/manga", "15" },
{ "/ost", "16" },
{ "/podcast", "17" },
{ "/anime_movie", "3" },
{ "/anime_ova/anime_ona", "5" },
{ "/anime_ova", "4" },
{ "/dorama/japan_dorama", "6" },
{ "/dorama/korea_dorama", "7" },
{ "/dorama/china_dorama", "8" },
{ "/dorama", "9" },
{ "/anons_ongoing", "12" }
};
public AnidubParser(AnidubSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
private static string GetTitle(AngleSharp.Html.Dom.IHtmlDocument content, AngleSharp.Dom.IElement tabNode)
{
var domTitle = content.QuerySelector("#news-title");
var baseTitle = domTitle.TextContent.Trim();
var quality = GetQuality(tabNode.ParentElement);
if (!string.IsNullOrWhiteSpace(quality))
{
return $"{baseTitle} [{quality}]";
}
return baseTitle;
}
private static string GetQuality(AngleSharp.Dom.IElement releaseNode)
{
// For some releases there's no block with quality
if (string.IsNullOrWhiteSpace(releaseNode.Id))
{
return null;
}
var quality = releaseNode.Id.Trim();
switch (quality.ToLowerInvariant())
{
case "tv720":
return "HDTV 720p";
case "tv1080":
return "HDTV 1080p";
case "bd720":
return "BDRip 720p";
case "bd1080":
return "BDRip 1080p";
case "hwp":
return "SDTV";
default:
return quality.ToUpperInvariant();
}
}
private static int GetReleaseLeechers(AngleSharp.Dom.IElement tabNode)
{
const string LeechersSelector = ".list.down > .li_swing_m";
var leechersStr = tabNode.QuerySelector(LeechersSelector).TextContent;
int.TryParse(leechersStr, out var leechers);
return leechers;
}
private static int GetReleaseSeeders(AngleSharp.Dom.IElement tabNode)
{
const string SeedersSelector = ".list.down > .li_distribute_m";
var seedersStr = tabNode.QuerySelector(SeedersSelector).TextContent;
int.TryParse(seedersStr, out var seeders);
return seeders;
}
private static int GetReleaseGrabs(AngleSharp.Dom.IElement tabNode)
{
const string GrabsSelector = ".list.down > .li_download_m";
var grabsStr = tabNode.QuerySelector(GrabsSelector).TextContent;
int.TryParse(grabsStr, out var grabs);
return grabs;
}
private static string GetDateFromDocument(AngleSharp.Html.Dom.IHtmlDocument content)
{
const string DateSelector = ".story_inf > li:nth-child(2)";
var domDate = content.QuerySelector(DateSelector).LastChild;
if (domDate?.NodeName != "#text")
{
return string.Empty;
}
return domDate.NodeValue.Trim();
}
private DateTime GetDateFromShowPage(AngleSharp.Html.Dom.IHtmlDocument content)
{
const string dateFormat = "d-MM-yyyy";
const string dateTimeFormat = dateFormat + ", HH:mm";
// Would be better to use AssumeLocal and provide "ru-RU" culture,
// but doesn't work cross-platform
const DateTimeStyles style = DateTimeStyles.AssumeUniversal;
var culture = CultureInfo.InvariantCulture;
var dateText = GetDateFromDocument(content);
//Correct way but will not always work on cross-platform
//var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
//var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, localTimeZone);
// Russian Standard Time is +03:00, no DST
const int russianStandardTimeDiff = 3;
var nowLocal = DateTime.UtcNow.AddHours(russianStandardTimeDiff);
dateText = dateText
.Replace("Вчера", nowLocal.AddDays(-1).ToString(dateFormat))
.Replace("Сегодня", nowLocal.ToString(dateFormat));
if (DateTime.TryParseExact(dateText, dateTimeFormat, culture, style, out var date))
{
var utcDate = date.ToUniversalTime();
return utcDate.AddHours(-russianStandardTimeDiff);
}
Logger.Warn($"[AniDub] Date time couldn't be parsed on. Date text: {dateText}");
return DateTime.UtcNow;
}
private static long GetReleaseSize(AngleSharp.Dom.IElement tabNode)
{
const string SizeSelector = ".list.down > .red";
var sizeStr = tabNode.QuerySelector(SizeSelector).TextContent;
return ReleaseInfo.GetBytes(sizeStr);
}
private string GetReleaseLink(AngleSharp.Dom.IElement tabNode)
{
return $"{_settings.BaseUrl}engine/download.php?id={GetTorrentId(tabNode)}";
}
private static string GetTorrentId(AngleSharp.Dom.IElement tabNode)
{
var nodeId = tabNode.Id;
// Format is "torrent_{id}_info"
return nodeId
.Replace("torrent_", string.Empty)
.Replace("_info", string.Empty);
}
private ICollection<IndexerCategory> ParseCategories(string uriPath)
{
var categoriesMap = CategoriesMap;
return categoriesMap
.Where(categoryMap => uriPath.StartsWith(categoryMap.Key))
.Select(categoryMap => _categories.MapTrackerCatToNewznab(categoryMap.Value))
.FirstOrDefault();
}
private IList<TorrentInfo> ParseRelease(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
foreach (var t in dom.QuerySelectorAll("#tabs .torrent_c > div"))
{
var release = new TorrentInfo
{
Title = GetTitle(dom, t),
InfoUrl = indexerResponse.Request.Url.ToString(),
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
Guid = indexerResponse.Request.Url.ToString() + t.Id,
Seeders = GetReleaseSeeders(t),
Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t),
Grabs = GetReleaseGrabs(t),
Categories = ParseCategories(indexerResponse.Request.Url.Path),
PublishDate = GetDateFromShowPage(dom),
DownloadUrl = GetReleaseLink(t),
Size = GetReleaseSize(t),
Resolution = GetQuality(t)
};
torrentInfos.Add(release);
}
return torrentInfos;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var domQuery = string.Empty;
if (indexerResponse.Request.Url.Query.Contains("do=search"))
{
domQuery = ".searchitem > h3 > a";
}
else
{
domQuery = "#dle-content > .story > .story_h > .lcol > h2 > a";
}
var links = dom.QuerySelectorAll(domQuery);
foreach (var link in links)
{
var url = link.GetAttribute("href");
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
// Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError)
{
if ((int)releaseResponse.HttpResponse.StatusCode == 429)
{
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
}
}
torrentInfos.AddRange(ParseRelease(releaseResponse));
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnidubSettingsValidator : AbstractValidator<AnidubSettings>
{
public AnidubSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
}
}
public class AnidubSettings : IIndexerSettings
{
private static readonly AnidubSettingsValidator Validator = new AnidubSettingsValidator();
public AnidubSettings()
{
Username = "";
Password = "";
}
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Anilibria : TorrentIndexerBase<AnilibriaSettings>
{
public override string Name => "Anilibria";
public override string[] IndexerUrls => new string[] { "https://anilibria.tv/" };
public override string Description => "Anilibria is russian anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-ru";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anilibria(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnilibriaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnilibriaParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "ТВ");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVAnime, "ONA");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVAnime, "OVA");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Фильм");
return caps;
}
}
public class AnilibriaRequestGenerator : IIndexerRequestGenerator
{
public AnilibriaSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public AnilibriaRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var apiUrl = Regex.Replace(Settings.BaseUrl, @"(https?:\/\/)(.*)", "$1api.$2v2");
var queryCollection = new NameValueCollection
{
{ "limit", "100" },
{ "filter", "names,code,torrents.list,season.year,type.string" }
};
if (string.IsNullOrWhiteSpace(term))
{
apiUrl += "/getUpdates?" + queryCollection.GetQueryString();
}
else
{
apiUrl += "/searchTitles?" + queryCollection.GetQueryString() + "&search=" + Uri.EscapeDataString(term);
}
var request = new IndexerRequest(apiUrl, HttpAccept.Json);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
// Anilibria doesn't support music, but this function required by interface
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
// Anilibria doesn't support books, but this function required by interface
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnilibriaParser : IParseIndexerResponse
{
private readonly AnilibriaSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AnilibriaParser(AnilibriaSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
private string composeTitle(AnilibriaTitle tl, AnilibriaTorrent tr)
{
var title = tl.Names.Ru;
title += " / " + tl.Names.En;
if (tl.Names.Alternative is string)
{
title += " / " + tl.Names.Alternative;
}
title += " " + tl.Season.Year;
title += " [" + tr.Quality.String + "]";
if (!string.IsNullOrWhiteSpace(tr.Series.String))
{
title += " - E" + tr.Series.String;
}
return title;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var queryResponseItems = JsonConvert.DeserializeObject<List<AnilibriaTitle>>(indexerResponse.Content);
foreach (var tl in queryResponseItems)
{
foreach (var tr in tl.Torrents.List)
{
var torrentInfo = new TorrentInfo
{
Title = composeTitle(tl, tr),
InfoUrl = string.Format("{0}/release/{1}.html", _settings.BaseUrl.TrimEnd('/'), tl.Code),
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
Seeders = tr.Seeders,
Peers = tr.Leechers + tr.Seeders,
Grabs = tr.Downloads,
Categories = _categories.MapTrackerCatDescToNewznab(tl.Type.String),
// API provides timestamp in UTC+3 timezone, so we need to substract 3 hours
PublishDate = DateTimeUtil.UnixTimestampToDateTime(tr.UploadedTimestamp).AddHours(-3),
Guid = _settings.BaseUrl + tr.Url,
DownloadUrl = _settings.BaseUrl + tr.Url,
Size = tr.TotalSize,
Resolution = tr.Quality.Resolution,
Codec = tr.Quality.Encoder
};
torrentInfos.Add(torrentInfo);
}
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnilibriaSettingsValidator : AbstractValidator<AnilibriaSettings>
{
public AnilibriaSettingsValidator()
{
}
}
public class AnilibriaSettings : IIndexerSettings
{
private static readonly AnilibriaSettingsValidator Validator = new AnilibriaSettingsValidator();
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public class AnilibriaTitle
{
public AnilibriaNames Names { get; set; }
public string Code { get; set; }
public AnilibriaTorrents Torrents { get; set; }
public AnilibriaSeason Season { get; set; }
public AnilibriaTitleType Type { get; set; }
}
public class AnilibriaTitleType
{
public string String { get; set; }
}
public class AnilibriaNames
{
public string Ru { get; set; }
public string En { get; set; }
public object Alternative { get; set; }
}
public class AnilibriaSeason
{
public long Year { get; set; }
}
public class AnilibriaTorrents
{
public AnilibriaTorrent[] List { get; set; }
}
public class AnilibriaTorrent
{
public AnilibriaSeries Series { get; set; }
public AnilibriaQuality Quality { get; set; }
public int Leechers { get; set; }
public int Seeders { get; set; }
public int Downloads { get; set; }
[JsonProperty("total_size")]
public long TotalSize { get; set; }
public string Url { get; set; }
[JsonProperty("uploaded_timestamp")]
public long UploadedTimestamp { get; set; }
}
public class AnilibriaQuality
{
public string String { get; set; }
public string Type { get; set; }
public string Resolution { get; set; }
public string Encoder { get; set; }
}
public class AnilibriaSeries
{
public long First { get; set; }
public long Last { get; set; }
public string String { get; set; }
}
}

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Text.RegularExpressions;
using FluentValidation;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Http;
@@ -18,7 +19,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
@@ -26,8 +26,8 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AnimeBytes : TorrentIndexerBase<AnimeBytesSettings>
{
public override string Name => "AnimeBytes";
public override string BaseUrl => "https://animebytes.tv/";
public override string Description => "Powered by Tentacles";
public override string[] IndexerUrls => new string[] { "https://animebytes.tv/" };
public override string Description => "AnimeBytes (AB) is the largest private torrent tracker that specialises in anime and anime-related content.";
public override string Language => "en-us";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
@@ -41,12 +41,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimeBytesRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
return new AnimeBytesRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnimeBytesParser(Settings, Capabilities.Categories, BaseUrl);
return new AnimeBytesParser(Settings, Capabilities.Categories);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
@@ -101,7 +101,6 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public AnimeBytesSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public string BaseUrl { get; set; }
public AnimeBytesRequestGenerator()
{
@@ -109,14 +108,14 @@ namespace NzbDrone.Core.Indexers.Definitions
private IEnumerable<IndexerRequest> GetPagedRequests(string searchType, string term, int[] categories)
{
var searchUrl = string.Format("{0}/scrape.php", BaseUrl.TrimEnd('/'));
var searchUrl = string.Format("{0}/scrape.php", Settings.BaseUrl.TrimEnd('/'));
var queryCollection = new NameValueCollection
{
{ "username", Settings.Username },
{ "torrent_pass", Settings.Passkey },
{ "type", searchType },
{ "searchstr", term }
{ "searchstr", StripEpisodeNumber(term) }
};
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
@@ -183,19 +182,26 @@ namespace NzbDrone.Core.Indexers.Definitions
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private string StripEpisodeNumber(string term)
{
// Tracer does not support searching with episode number so strip it if we have one
term = Regex.Replace(term, @"\W(\dx)?\d?\d$", string.Empty);
term = Regex.Replace(term, @"\W(S\d\d?E)?\d?\d$", string.Empty);
term = Regex.Replace(term, @"\W\d+$", string.Empty);
return term;
}
}
public class AnimeBytesParser : IParseIndexerResponse
{
private readonly AnimeBytesSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly string _baseUrl;
public AnimeBytesParser(AnimeBytesSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
public AnimeBytesParser(AnimeBytesSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
_baseUrl = baseUrl;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -212,29 +218,17 @@ namespace NzbDrone.Core.Indexers.Definitions
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
//TODO: Create API Resource Type
var json = JsonConvert.DeserializeObject<dynamic>(indexerResponse.Content);
var response = JsonConvert.DeserializeObject<AnimeBytesResponse>(indexerResponse.Content);
if (json["error"] != null)
if (response.Matches > 0)
{
throw new Exception(json["error"].ToString());
}
var matches = (long)json["Matches"];
if (matches > 0)
{
var groups = (JArray)json.Groups;
foreach (var group in groups)
foreach (var group in response.Groups)
{
var synonyms = new List<string>();
var posterStr = (string)group["Image"];
var poster = string.IsNullOrWhiteSpace(posterStr) ? null : new Uri(posterStr);
var year = (int)group["Year"];
var groupName = (string)group["GroupName"];
var seriesName = (string)group["SeriesName"];
var mainTitle = WebUtility.HtmlDecode((string)group["FullName"]);
var year = group.Year;
var groupName = group.GroupName;
var seriesName = group.SeriesName;
var mainTitle = WebUtility.HtmlDecode(group.FullName);
if (seriesName != null)
{
mainTitle = seriesName;
@@ -242,105 +236,111 @@ namespace NzbDrone.Core.Indexers.Definitions
synonyms.Add(mainTitle);
// TODO: Do we need all these options?
//if (group["Synonymns"].HasValues)
//{
// if (group["Synonymns"] is JArray)
// {
// var allSyonyms = group["Synonymns"].ToObject<List<string>>();
// if (AddJapaneseTitle && allSyonyms.Count >= 1)
// synonyms.Add(allSyonyms[0]);
// if (AddRomajiTitle && allSyonyms.Count >= 2)
// synonyms.Add(allSyonyms[1]);
// if (AddAlternativeTitles && allSyonyms.Count >= 3)
// synonyms.AddRange(allSyonyms[2].Split(',').Select(t => t.Trim()));
// }
// else
// {
// var allSynonyms = group["Synonymns"].ToObject<Dictionary<int, string>>();
// if (AddJapaneseTitle && allSynonyms.ContainsKey(0))
// synonyms.Add(allSynonyms[0]);
// if (AddRomajiTitle && allSynonyms.ContainsKey(1))
// synonyms.Add(allSynonyms[1]);
// if (AddAlternativeTitles && allSynonyms.ContainsKey(2))
// {
// synonyms.AddRange(allSynonyms[2].Split(',').Select(t => t.Trim()));
// }
// }
//}
List<IndexerCategory> category = null;
var categoryName = (string)group["CategoryName"];
var description = (string)group["Description"];
foreach (var torrent in group["Torrents"])
if (group.Synonymns.StringArray != null)
{
var releaseInfo = "S01";
string episode = null;
synonyms.AddRange(group.Synonymns.StringArray);
}
else
{
synonyms.AddRange(group.Synonymns.StringMap.Values);
}
List<IndexerCategory> category = null;
var categoryName = group.CategoryName;
var description = group.Description;
foreach (var torrent in group.Torrents)
{
var releaseInfo = _settings.EnableSonarrCompatibility ? "S01" : "";
int? episode = null;
int? season = null;
var editionTitle = (string)torrent["EditionData"]["EditionTitle"];
var editionTitle = torrent.EditionData.EditionTitle;
if (!string.IsNullOrWhiteSpace(editionTitle))
{
releaseInfo = WebUtility.HtmlDecode(editionTitle);
if (_settings.EnableSonarrCompatibility)
{
var simpleSeasonRegEx = new Regex(@"Season (\d+)", RegexOptions.Compiled);
var simpleSeasonRegExMatch = simpleSeasonRegEx.Match(releaseInfo);
if (simpleSeasonRegExMatch.Success)
{
season = ParseUtil.CoerceInt(simpleSeasonRegExMatch.Groups[1].Value);
}
}
var episodeRegEx = new Regex(@"Episode (\d+)", RegexOptions.Compiled);
var episodeRegExMatch = episodeRegEx.Match(releaseInfo);
if (episodeRegExMatch.Success)
{
episode = ParseUtil.CoerceInt(episodeRegExMatch.Groups[1].Value);
}
}
var seasonRegEx = new Regex(@"Season (\d+)", RegexOptions.Compiled);
var seasonRegExMatch = seasonRegEx.Match(releaseInfo);
if (seasonRegExMatch.Success)
if (_settings.EnableSonarrCompatibility)
{
season = ParseUtil.CoerceInt(seasonRegExMatch.Groups[1].Value);
var advancedSeasonRegEx = new Regex(@"(\d+)(st|nd|rd|th) Season", RegexOptions.Compiled | RegexOptions.IgnoreCase);
var advancedSeasonRegExMatch = advancedSeasonRegEx.Match(mainTitle);
if (advancedSeasonRegExMatch.Success)
{
season = ParseUtil.CoerceInt(advancedSeasonRegExMatch.Groups[1].Value);
}
var seasonCharactersRegEx = new Regex(@"(I{2,})$", RegexOptions.Compiled);
var seasonCharactersRegExMatch = seasonCharactersRegEx.Match(mainTitle);
if (seasonCharactersRegExMatch.Success)
{
season = seasonCharactersRegExMatch.Groups[1].Value.Length;
}
var seasonNumberRegEx = new Regex(@"([2-9])$", RegexOptions.Compiled);
var seasonNumberRegExMatch = seasonNumberRegEx.Match(mainTitle);
if (seasonNumberRegExMatch.Success)
{
season = ParseUtil.CoerceInt(seasonNumberRegExMatch.Groups[1].Value);
}
}
var episodeRegEx = new Regex(@"Episode (\d+)", RegexOptions.Compiled);
var episodeRegExMatch = episodeRegEx.Match(releaseInfo);
if (episodeRegExMatch.Success)
if (episode != null)
{
episode = episodeRegExMatch.Groups[1].Value;
releaseInfo = episode is > 0 and < 10
? "0" + episode
: episode.ToString();
}
else
{
if (season != null && _settings.EnableSonarrCompatibility)
{
releaseInfo = $"S{season}";
}
}
releaseInfo = releaseInfo.Replace("Episode ", "");
releaseInfo = releaseInfo.Replace("Season ", "S");
releaseInfo = releaseInfo.Trim();
//if (PadEpisode && int.TryParse(releaseInfo, out _) && releaseInfo.Length == 1)
//{
// releaseInfo = "0" + releaseInfo;
//}
//if (FilterSeasonEpisode)
//{
// if (query.Season != 0 && season != null && season != query.Season) // skip if season doesn't match
// continue;
// if (query.Episode != null && episode != null && episode != query.Episode) // skip if episode doesn't match
// continue;
//}
var torrentId = (long)torrent["ID"];
var property = ((string)torrent["Property"]).Replace(" | Freeleech", "");
var link = (string)torrent["Link"];
var linkUri = new Uri(link);
var uploadTimeString = (string)torrent["UploadTime"];
var uploadTime = DateTime.ParseExact(uploadTimeString, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
var publishDate = DateTime.SpecifyKind(uploadTime, DateTimeKind.Utc).ToLocalTime();
var details = new Uri(_baseUrl + "torrent/" + torrentId + "/group");
var size = (long)torrent["Size"];
var snatched = (int)torrent["Snatched"];
var seeders = (int)torrent["Seeders"];
var leechers = (int)torrent["Leechers"];
var fileCount = (int)torrent["FileCount"];
var torrentId = torrent.Id;
var property = torrent.Property.Replace(" | Freeleech", string.Empty);
var link = torrent.Link;
var uploadTime = torrent.UploadTime;
var publishDate = DateTime.SpecifyKind(uploadTime.DateTime, DateTimeKind.Utc).ToLocalTime();
var details = new Uri(_settings.BaseUrl + "torrent/" + torrentId + "/group");
var size = torrent.Size;
var snatched = torrent.Snatched;
var seeders = torrent.Seeders;
var leechers = torrent.Leechers;
var fileCount = torrent.FileCount;
var peers = seeders + leechers;
var rawDownMultiplier = (int?)torrent["RawDownMultiplier"] ?? 0;
var rawUpMultiplier = (int?)torrent["RawUpMultiplier"] ?? 0;
var rawDownMultiplier = torrent.RawDownMultiplier;
var rawUpMultiplier = torrent.RawUpMultiplier;
// Ignore these categories as they'll cause hell with the matcher
// TV Special, ONA, DVD Special, BD Special
if (groupName == "TV Series" || groupName == "OVA")
{
category = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
}
// Ignore these categories as they'll cause hell with the matcher
// TV Special, OVA, ONA, DVD Special, BD Special
if (groupName == "Movie" || groupName == "Live Action Movie")
{
category = new List<IndexerCategory> { NewznabStandardCategory.Movies };
@@ -426,7 +426,7 @@ namespace NzbDrone.Core.Indexers.Definitions
//{
// continue;
//}
var infoString = releaseTags.Aggregate("", (prev, cur) => prev + "[" + cur + "]");
var infoString = releaseTags.Aggregate(string.Empty, (prev, cur) => prev + "[" + cur + "]");
var minimumSeedTime = 259200;
// Additional 5 hours per GB
@@ -447,7 +447,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Title = releaseTitle,
InfoUrl = details.AbsoluteUri,
Guid = guid.AbsoluteUri,
DownloadUrl = linkUri.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
PublishDate = publishDate,
Categories = category,
Description = description,
@@ -457,7 +457,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Grabs = snatched,
Files = fileCount,
DownloadVolumeFactor = rawDownMultiplier,
UploadVolumeFactor = rawUpMultiplier
UploadVolumeFactor = rawUpMultiplier,
};
torrentInfos.Add(release);
@@ -484,7 +484,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class AnimeBytesSettings : IProviderConfig
public class AnimeBytesSettings : IIndexerSettings
{
private static readonly AnimeBytesSettingsValidator Validator = new AnimeBytesSettingsValidator();
@@ -492,17 +492,321 @@ namespace NzbDrone.Core.Indexers.Definitions
{
Passkey = "";
Username = "";
EnableSonarrCompatibility = true;
}
[FieldDefinition(1, Label = "Passkey", HelpText = "Site Passkey")]
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Passkey { get; set; }
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
[FieldDefinition(3, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
public bool EnableSonarrCompatibility { get; set; }
[FieldDefinition(5)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public class AnimeBytesResponse
{
[JsonProperty("Matches")]
public long Matches { get; set; }
[JsonProperty("Limit")]
public long Limit { get; set; }
[JsonProperty("Results")]
[JsonConverter(typeof(ParseStringConverter))]
public long Results { get; set; }
[JsonProperty("Groups")]
public Group[] Groups { get; set; }
}
public class Group
{
[JsonProperty("ID")]
public long Id { get; set; }
[JsonProperty("CategoryName")]
public string CategoryName { get; set; }
[JsonProperty("FullName")]
public string FullName { get; set; }
[JsonProperty("GroupName")]
public string GroupName { get; set; }
[JsonProperty("SeriesID")]
[JsonConverter(typeof(ParseStringConverter))]
public long SeriesId { get; set; }
[JsonProperty("SeriesName")]
public string SeriesName { get; set; }
[JsonProperty("Artists")]
public object Artists { get; set; }
[JsonProperty("Year")]
[JsonConverter(typeof(ParseStringConverter))]
public long Year { get; set; }
[JsonProperty("Image")]
public Uri Image { get; set; }
[JsonProperty("Synonymns")]
[JsonConverter(typeof(SynonymnsConverter))]
public Synonymns Synonymns { get; set; }
[JsonProperty("Snatched")]
public long Snatched { get; set; }
[JsonProperty("Comments")]
public long Comments { get; set; }
[JsonProperty("Links")]
[JsonConverter(typeof(LinksUnionConverter))]
public LinksUnion Links { get; set; }
[JsonProperty("Votes")]
public long Votes { get; set; }
[JsonProperty("AvgVote")]
public double AvgVote { get; set; }
[JsonProperty("Associations")]
public object Associations { get; set; }
[JsonProperty("Description")]
public string Description { get; set; }
[JsonProperty("DescriptionHTML")]
public string DescriptionHtml { get; set; }
[JsonProperty("EpCount")]
public long EpCount { get; set; }
[JsonProperty("StudioList")]
public string StudioList { get; set; }
[JsonProperty("PastWeek")]
public long PastWeek { get; set; }
[JsonProperty("Incomplete")]
public bool Incomplete { get; set; }
[JsonProperty("Ongoing")]
public bool Ongoing { get; set; }
[JsonProperty("Tags")]
public List<string> Tags { get; set; }
[JsonProperty("Torrents")]
public List<Torrent> Torrents { get; set; }
}
public class LinksClass
{
[JsonProperty("ANN", NullValueHandling = NullValueHandling.Ignore)]
public Uri Ann { get; set; }
[JsonProperty("Manga-Updates", NullValueHandling = NullValueHandling.Ignore)]
public Uri MangaUpdates { get; set; }
[JsonProperty("Wikipedia", NullValueHandling = NullValueHandling.Ignore)]
public Uri Wikipedia { get; set; }
[JsonProperty("MAL", NullValueHandling = NullValueHandling.Ignore)]
public Uri Mal { get; set; }
[JsonProperty("AniDB", NullValueHandling = NullValueHandling.Ignore)]
public Uri AniDb { get; set; }
}
public class Torrent
{
[JsonProperty("ID")]
public long Id { get; set; }
[JsonProperty("EditionData")]
public EditionData EditionData { get; set; }
[JsonProperty("RawDownMultiplier")]
public double? RawDownMultiplier { get; set; }
[JsonProperty("RawUpMultiplier")]
public double? RawUpMultiplier { get; set; }
[JsonProperty("Link")]
public Uri Link { get; set; }
[JsonProperty("Property")]
public string Property { get; set; }
[JsonProperty("Snatched")]
public int Snatched { get; set; }
[JsonProperty("Seeders")]
public int Seeders { get; set; }
[JsonProperty("Leechers")]
public int Leechers { get; set; }
[JsonProperty("Size")]
public long Size { get; set; }
[JsonProperty("FileCount")]
public int FileCount { get; set; }
[JsonProperty("UploadTime")]
public DateTimeOffset UploadTime { get; set; }
}
public class EditionData
{
[JsonProperty("EditionTitle")]
public string EditionTitle { get; set; }
}
public struct LinksUnion
{
public List<object> AnythingArray;
public LinksClass LinksClass;
public static implicit operator LinksUnion(List<object> anythingArray) => new LinksUnion { AnythingArray = anythingArray };
public static implicit operator LinksUnion(LinksClass linksClass) => new LinksUnion { LinksClass = linksClass };
}
public struct Synonymns
{
public List<string> StringArray;
public Dictionary<string, string> StringMap;
public static implicit operator Synonymns(List<string> stringArray) => new Synonymns { StringArray = stringArray };
public static implicit operator Synonymns(Dictionary<string, string> stringMap) => new Synonymns { StringMap = stringMap };
}
internal class LinksUnionConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(LinksUnion) || t == typeof(LinksUnion?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.StartObject:
var objectValue = serializer.Deserialize<LinksClass>(reader);
return new LinksUnion { LinksClass = objectValue };
case JsonToken.StartArray:
var arrayValue = serializer.Deserialize<List<object>>(reader);
return new LinksUnion { AnythingArray = arrayValue };
}
throw new Exception("Cannot unmarshal type LinksUnion");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (LinksUnion)untypedValue;
if (value.AnythingArray != null)
{
serializer.Serialize(writer, value.AnythingArray);
return;
}
if (value.LinksClass == null)
{
throw new Exception("Cannot marshal type LinksUnion");
}
serializer.Serialize(writer, value.LinksClass);
}
public static readonly LinksUnionConverter Singleton = new LinksUnionConverter();
}
internal class ParseStringConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var value = serializer.Deserialize<string>(reader);
if (long.TryParse(value, out var l))
{
return l;
}
throw new Exception("Cannot unmarshal type long");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
if (untypedValue == null)
{
serializer.Serialize(writer, null);
return;
}
var value = (long)untypedValue;
serializer.Serialize(writer, value.ToString());
}
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
}
internal class SynonymnsConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(Synonymns) || t == typeof(Synonymns?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.StartObject:
var objectValue = serializer.Deserialize<Dictionary<string, string>>(reader);
return new Synonymns { StringMap = objectValue };
case JsonToken.StartArray:
var arrayValue = serializer.Deserialize<List<string>>(reader);
return new Synonymns { StringArray = arrayValue };
}
throw new Exception("Cannot unmarshal type Synonymns");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (Synonymns)untypedValue;
if (value.StringArray != null)
{
serializer.Serialize(writer, value.StringArray);
return;
}
if (value.StringMap == null)
{
throw new Exception("Cannot marshal type Synonymns");
}
serializer.Serialize(writer, value.StringMap);
}
public static readonly SynonymnsConverter Singleton = new SynonymnsConverter();
}
}

View File

@@ -16,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
@@ -25,8 +24,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "AnimeTorrents";
public override string BaseUrl => "https://animetorrents.me/";
private string LoginUrl => BaseUrl + "login.php";
public override string[] IndexerUrls => new string[] { "https://animetorrents.me/" };
public override string Description => "Definitive source for anime and manga";
private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -38,12 +38,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnimeTorrentsParser(Settings, Capabilities.Categories, BaseUrl);
return new AnimeTorrentsParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
@@ -135,7 +135,6 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public AnimeTorrentsSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public string BaseUrl { get; set; }
public AnimeTorrentsRequestGenerator()
{
@@ -148,8 +147,8 @@ namespace NzbDrone.Core.Indexers.Definitions
// replace any space, special char, etc. with % (wildcard)
var replaceRegex = new Regex("[^a-zA-Z0-9]+");
searchString = replaceRegex.Replace(searchString, "%");
var searchUrl = BaseUrl + "ajax/torrents_data.php";
var searchUrlReferer = BaseUrl + "torrents.php?cat=0&searchin=filename&search=";
var searchUrl = Settings.BaseUrl + "ajax/torrents_data.php";
var searchUrlReferer = Settings.BaseUrl + "torrents.php?cat=0&searchin=filename&search=";
var trackerCats = Capabilities.Categories.MapTorznabCapsToTrackers(categories) ?? new List<string>();
@@ -229,13 +228,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly AnimeTorrentsSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly string _baseUrl;
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
_baseUrl = baseUrl;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -340,7 +337,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class AnimeTorrentsSettings : IProviderConfig
public class AnimeTorrentsSettings : IIndexerSettings
{
private static readonly AnimeTorrentsSettingsValidator Validator = new AnimeTorrentsSettingsValidator();
@@ -350,12 +347,18 @@ namespace NzbDrone.Core.Indexers.Definitions
Password = "";
}
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Site password", Privacy = PrivacyLevel.Password)]
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AnimeWorld : Unit3dBase
{
public override string Name => "AnimeWorld";
public override string BaseUrl => "https://animeworld.cx/";
public override string[] IndexerUrls => new string[] { "https://animeworld.cx/" };
public override string Description => "AnimeWorld (AW) is a GERMAN Private site for ANIME / MANGA / HENTAI";
public override string Language => "de-de";

View File

@@ -0,0 +1,350 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Animedia : TorrentIndexerBase<AnimediaSettings>
{
public override string Name => "Animedia";
public override string[] IndexerUrls => new string[] { "https://tt.animedia.tv/" };
public override string Description => "Animedia is russian anime voiceover group and eponymous anime tracker.";
public override string Language => "ru-ru";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Animedia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimediaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnimediaParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV Anime");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA/ONA/Special");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Dorama");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Movies");
return caps;
}
}
public class AnimediaRequestGenerator : IIndexerRequestGenerator
{
public AnimediaSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public AnimediaRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var requestUrl = string.Empty;
if (string.IsNullOrWhiteSpace(term))
{
requestUrl = Settings.BaseUrl;
}
else
{
var queryCollection = new NameValueCollection
{
// Remove season and episode info from search term cause it breaks search
{ "keywords", Regex.Replace(term, @"(?:[SsEe]?\d{1,4}){1,2}$", "").TrimEnd() },
{ "limit", "20" },
{ "orderby_sort", "entry_date|desc" }
};
requestUrl = string.Format("{0}/ajax/search_result/P0?{1}", Settings.BaseUrl.TrimEnd('/'), queryCollection.GetQueryString());
}
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
// Animedia doesn't support music, but this function required by interface
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
// Animedia doesn't support books, but this function required by interface
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnimediaParser : IParseIndexerResponse
{
private readonly AnimediaSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private static readonly Regex EpisodesInfoQueryRegex = new Regex(@"сери[ия] (\d+)(?:-(\d+))? из.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ResolutionInfoQueryRegex = new Regex(@"качество (\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SizeInfoQueryRegex = new Regex(@"размер:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ReleaseDateInfoQueryRegex = new Regex(@"добавлен:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieMovieRegex = new Regex(@"Фильм", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public IHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
public AnimediaParser(AnimediaSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
private string composeTitle(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
{
var name_ru = dom.QuerySelector("div.media__post__header > h1").TextContent.Trim();
var name_en = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span").TextContent.Trim();
var name_orig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span").TextContent.Trim();
var title = name_ru + " / " + name_en;
if (name_en != name_orig)
{
title += " / " + name_orig;
}
var tabName = t.TextContent;
tabName = tabName.Replace("Сезон", "Season");
if (tabName.Contains("Серии"))
{
tabName = "";
}
var heading = tr.QuerySelector("h3.tracker_info_bold").TextContent;
// Parse episodes info from heading if episods info present
var match = EpisodesInfoQueryRegex.Match(heading);
heading = tabName;
if (match.Success)
{
if (string.IsNullOrEmpty(match.Groups[2].Value))
{
heading += " E" + match.Groups[1].Value;
}
else
{
heading += string.Format(" E{0}-{1}", match.Groups[1].Value, match.Groups[2].Value);
}
}
return title + " - " + heading + " [" + getResolution(tr) + "p]";
}
private string getResolution(AngleSharp.Dom.IElement tr)
{
var resolution = tr.QuerySelector("div.tracker_info_left").TextContent;
return ResolutionInfoQueryRegex.Match(resolution).Groups[1].Value;
}
private long getReleaseSize(AngleSharp.Dom.IElement tr)
{
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
return ReleaseInfo.GetBytes(SizeInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
}
private DateTime getReleaseDate(AngleSharp.Dom.IElement tr)
{
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
return DateTime.Parse(ReleaseDateInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
}
private ICollection<IndexerCategory> MapCategories(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
{
var rName = t.TextContent;
var rDesc = tr.QuerySelector("h3.tracker_info_bold").TextContent;
var type = dom.QuerySelector("div.releases-date:contains('Тип:')").TextContent;
// Check OVA first cause OVA looks like anime with OVA in release name or description
if (CategorieOVARegex.IsMatch(rName) || CategorieOVARegex.IsMatch(rDesc))
{
return _categories.MapTrackerCatDescToNewznab("OVA/ONA/Special");
}
// Check movies then, cause some of releases could be movies dorama and should go to movies category
if (CategorieMovieRegex.IsMatch(rName) || CategorieMovieRegex.IsMatch(rDesc))
{
return _categories.MapTrackerCatDescToNewznab("Movies");
}
// Check dorama. Most of doramas are flaged as doramas in type info, but type info could have a lot of types at same time (movie, etc)
if (CategorieDoramaRegex.IsMatch(rName) || CategorieDoramaRegex.IsMatch(type))
{
return _categories.MapTrackerCatDescToNewznab("Dorama");
}
return _categories.MapTrackerCatDescToNewznab("TV Anime");
}
private IList<TorrentInfo> ParseRelease(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
foreach (var t in dom.QuerySelectorAll("ul.media__tabs__nav > li > a"))
{
var tr_id = t.Attributes["href"].Value;
var tr = dom.QuerySelector("div" + tr_id);
var seeders = int.Parse(tr.QuerySelector("div.circle_green_text_top").TextContent);
var url = indexerResponse.HttpRequest.Url.ToString();
var release = new TorrentInfo
{
Title = composeTitle(dom, t, tr),
InfoUrl = url,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
Guid = url + tr_id,
Seeders = seeders,
Peers = seeders + int.Parse(tr.QuerySelector("div.circle_red_text_top").TextContent),
Grabs = int.Parse(tr.QuerySelector("div.circle_grey_text_top").TextContent),
Categories = MapCategories(dom, t, tr),
PublishDate = getReleaseDate(tr),
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").Attributes["href"].Value,
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").Attributes["href"].Value,
Size = getReleaseSize(tr),
Resolution = getResolution(tr)
};
torrentInfos.Add(release);
}
return torrentInfos;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var links = dom.QuerySelectorAll("a.ads-list__item__title");
foreach (var link in links)
{
var url = link.GetAttribute("href");
// Some URLs in search are broken
if (url.StartsWith("//"))
{
url = "https:" + url;
}
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
// Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError)
{
if ((int)releaseResponse.HttpResponse.StatusCode == 429)
{
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
}
}
torrentInfos.AddRange(ParseRelease(releaseResponse));
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnimediaSettingsValidator : AbstractValidator<AnimediaSettings>
{
public AnimediaSettingsValidator()
{
}
}
public class AnimediaSettings : IIndexerSettings
{
private static readonly AnimediaSettingsValidator Validator = new AnimediaSettingsValidator();
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,336 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Anthelion : TorrentIndexerBase<AnthelionSettings>
{
public override string Name => "Anthelion";
public override string[] IndexerUrls => new string[] { "https://anthelion.me/" };
private string LoginUrl => Settings.BaseUrl + "login.php";
public override string Description => "A movies tracker";
public override string Language => "en-us";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anthelion(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnthelionRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new AnthelionParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
};
requestBuilder.Method = HttpMethod.POST;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("keeplogged", "1")
.AddFormParameter("login", "Log+In!")
.SetHeader("Content-Type", "multipart/form-data")
.Build();
var headers = new NameValueCollection
{
{ "Referer", LoginUrl }
};
authLoginRequest.Headers.Add(headers);
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
{
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("form#loginform").TextContent.Trim();
throw new IndexerAuthException(errorMessage);
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Anthelion authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.Movies, "Film/Short");
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TV, "TV/Miniseries");
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Other, "Other");
return caps;
}
}
public class AnthelionRequestGenerator : IIndexerRequestGenerator
{
public AnthelionSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public AnthelionRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
// TODO: IMDB search is available but it requires to parse the details page
var qc = new NameValueCollection
{
{ "order_by", "time" },
{ "order_way", "desc" },
{ "action", "basic" },
{ "searchsubmit", "1" },
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term }
};
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
foreach (var cat in catList)
{
qc.Add($"filter_cat[{cat}]", "1");
}
searchUrl = searchUrl + "?" + qc.GetQueryString();
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnthelionParser : IParseIndexerResponse
{
private readonly AnthelionSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AnthelionParser(AnthelionSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var doc = parser.ParseDocument(indexerResponse.Content);
var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent");
foreach (var row in rows)
{
var qDetailsLink = row.QuerySelector("a.torrent_name");
var year = qDetailsLink.NextSibling.TextContent.Replace("[", "").Replace("]", "").Trim();
var tags = row.QuerySelector("div.torrent_info").FirstChild.TextContent.Replace(" / ", " ").Trim();
var title = $"{qDetailsLink.TextContent} {year} {tags}";
var description = row.QuerySelector("div.tags").TextContent.Trim();
var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href");
var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last();
var link = _settings.BaseUrl + "torrents.php?action=download&id=" + torrentId;
var posterStr = qDetailsLink.GetAttribute("data-cover");
var poster = !string.IsNullOrWhiteSpace(posterStr) ? posterStr : null;
var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent);
var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent);
var size = ReleaseInfo.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent);
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent);
var dlVolumeFactor = row.QuerySelector("strong.tl_free") != null ? 0 : 1;
var cat = row.QuerySelector("td.cats_col > div").GetAttribute("class").Replace("tooltip cats_", "");
var category = new List<IndexerCategory>
{
cat switch
{
"featurefilm" => NewznabStandardCategory.Movies,
"shortfilm" => NewznabStandardCategory.Movies,
"miniseries" => NewznabStandardCategory.TV,
"other" => NewznabStandardCategory.Other,
_ => throw new Exception($"Unknown category: {cat}")
}
};
// TODO: TMDb is also available
var qImdb = row.QuerySelector("a[href^=\"https://www.imdb.com\"]");
var imdb = qImdb != null ? ParseUtil.GetImdbID(qImdb.GetAttribute("href").Split('/').Last()) : null;
var release = new TorrentInfo
{
MinimumRatio = 1,
MinimumSeedTime = 259200,
Description = description,
Title = title,
PublishDate = publishDate,
Categories = category,
DownloadUrl = link,
InfoUrl = details,
PosterUrl = poster,
Guid = link,
ImdbId = imdb.GetValueOrDefault(),
Seeders = seeders,
Peers = leechers + seeders,
Size = size,
Grabs = grabs,
Files = files,
DownloadVolumeFactor = dlVolumeFactor,
UploadVolumeFactor = 1
};
torrentInfos.Add(release);
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnthelionSettingsValidator : AbstractValidator<AnthelionSettings>
{
public AnthelionSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
}
}
public class AnthelionSettings : IIndexerSettings
{
private static readonly AnthelionSettingsValidator Validator = new AnthelionSettingsValidator();
public AnthelionSettings()
{
Username = "";
Password = "";
}
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AvistaZ : AvistazBase
{
public override string Name => "AvistaZ";
public override string BaseUrl => "https://avistaz.to/";
public override string[] IndexerUrls => new string[] { "https://avistaz.to/" };
public override string Description => "Aka AsiaTorrents";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AvistaZ(IIndexerRepository indexerRepository, IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
@@ -25,8 +26,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Settings = Settings,
HttpClient = _httpClient,
Logger = _logger,
Capabilities = Capabilities,
BaseUrl = BaseUrl
Capabilities = Capabilities
};
}

View File

@@ -12,8 +12,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public abstract class AvistazBase : TorrentIndexerBase<AvistazSettings>
{
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override string BaseUrl => "";
protected virtual string LoginUrl => BaseUrl + "api/v1/jackett/auth";
public override string[] IndexerUrls => new string[] { "" };
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
public override int PageSize => 50;
@@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
Settings = Settings,
HttpClient = _httpClient,
Logger = _logger,
Capabilities = Capabilities,
BaseUrl = BaseUrl
Capabilities = Capabilities
};
}

View File

@@ -12,14 +12,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public class AvistazRequestGenerator : IIndexerRequestGenerator
{
public AvistazSettings Settings { get; set; }
public string BaseUrl { get; set; }
public IDictionary<string, string> AuthCookieCache { get; set; }
public IHttpClient HttpClient { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public Logger Logger { get; set; }
protected virtual string SearchUrl => BaseUrl + "api/v1/jackett/torrents";
protected virtual string SearchUrl => Settings.BaseUrl + "api/v1/jackett/torrents";
protected virtual bool ImdbInTags => false;
public Func<IDictionary<string, string>> GetCookies { get; set; }

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
@@ -11,10 +10,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(c => c.Pid).NotEmpty();
}
}
public class AvistazSettings : IProviderConfig
public class AvistazSettings : IIndexerSettings
{
private static readonly AvistazSettingsValidator Validator = new AvistazSettingsValidator();
@@ -25,15 +25,21 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public string Token { get; set; }
[FieldDefinition(1, Label = "Username", HelpText = "Username", Privacy = PrivacyLevel.UserName)]
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Password", Privacy = PrivacyLevel.Password)]
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(3, Label = "PID", HelpText = "PID from My Account or My Profile page")]
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
public string Pid { get; set; }
[FieldDefinition(5)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -16,7 +16,6 @@ using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
@@ -25,8 +24,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "BakaBT";
public override string BaseUrl => "https://bakabt.me/";
private string LoginUrl => BaseUrl + "login.php";
public override string[] IndexerUrls => new string[] { "https://bakabt.me/" };
public override string Description => "Anime Community";
private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -38,12 +38,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new BakaBTParser(Settings, Capabilities.Categories, BaseUrl);
return new BakaBTParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
@@ -110,6 +110,10 @@ namespace NzbDrone.Core.Indexers.Definitions
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
@@ -124,7 +128,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.AudioOther, "Soundtrack");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.BooksComics, "Manga");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVAnime, "Anime Movie");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Anime Movie");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVOther, "Live Action");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksOther, "Artbook");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.AudioVideo, "Music Video");
@@ -138,16 +142,15 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public BakaBTSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public string BaseUrl { get; set; }
public BakaBTRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
{
var searchString = term;
var searchUrl = BaseUrl + "browse.php?only=0&hentai=1&incomplete=1&lossless=1&hd=1&multiaudio=1&bonus=1&reorder=1&q=";
var searchUrl = Settings.BaseUrl + "browse.php?only=0&hentai=1&incomplete=1&lossless=1&hd=1&multiaudio=1&bonus=1&reorder=1&q=";
var match = Regex.Match(term, @".*(?=\s(?:[Ee]\d+|\d+)$)");
if (match.Success)
@@ -166,6 +169,8 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
return pageableRequests;
}
@@ -173,7 +178,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
return pageableRequests;
}
@@ -182,7 +187,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
return pageableRequests;
}
@@ -191,7 +196,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
return pageableRequests;
}
@@ -200,7 +205,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
return pageableRequests;
}
@@ -213,14 +218,12 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly BakaBTSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly string _baseUrl;
private readonly List<IndexerCategory> _defaultCategories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
_baseUrl = baseUrl;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -299,11 +302,11 @@ namespace NzbDrone.Core.Indexers.Definitions
release.Categories = currentCategories;
//release.Description = row.QuerySelector("span.tags")?.TextContent;
release.Guid = _baseUrl + qTitleLink.GetAttribute("href");
release.Description = row.QuerySelector("span.tags")?.TextContent;
release.Guid = _settings.BaseUrl + qTitleLink.GetAttribute("href");
release.InfoUrl = release.Guid;
release.DownloadUrl = _baseUrl + row.QuerySelector(".peers a").GetAttribute("href");
release.DownloadUrl = _settings.BaseUrl + row.QuerySelector(".peers a").GetAttribute("href");
var grabs = row.QuerySelectorAll(".peers")[0].FirstChild.NodeValue.TrimEnd().TrimEnd('/').TrimEnd();
grabs = grabs.Replace("k", "000");
@@ -392,7 +395,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class BakaBTSettings : IProviderConfig
public class BakaBTSettings : IIndexerSettings
{
private static readonly BakaBTSettingsValidator Validator = new BakaBTSettingsValidator();
@@ -402,18 +405,24 @@ namespace NzbDrone.Core.Indexers.Definitions
Password = "";
}
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Site password", Privacy = PrivacyLevel.Password)]
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(3, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
[FieldDefinition(4, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
public bool AddRomajiTitle { get; set; }
[FieldDefinition(4, Label = "Append Season", Type = FieldType.Checkbox, HelpText = "Append Season for Sonarr Compatibility")]
[FieldDefinition(5, Label = "Append Season", Type = FieldType.Checkbox, HelpText = "Append Season for Sonarr Compatibility")]
public bool AppendSeason { get; set; }
[FieldDefinition(6)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -16,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
@@ -25,7 +24,8 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "BeyondHD";
public override string BaseUrl => "https://beyond-hd.me/";
public override string[] IndexerUrls => new string[] { "https://beyond-hd.me/" };
public override string Description => "BeyondHD (BHD) is a Private Torrent Tracker for HD MOVIES / TV";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -37,12 +37,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new BeyondHDRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
return new BeyondHDRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new BeyondHDParser(Settings, Capabilities.Categories, BaseUrl);
return new BeyondHDParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
@@ -70,7 +70,6 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public BeyondHDSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public string BaseUrl { get; set; }
public BeyondHDRequestGenerator()
{
@@ -106,7 +105,7 @@ namespace NzbDrone.Core.Indexers.Definitions
body.Add("categories", string.Join(",", cats));
}
var searchUrl = BaseUrl + "api/torrents/" + Settings.ApiKey;
var searchUrl = Settings.BaseUrl + "api/torrents/" + Settings.ApiKey;
var request = new HttpRequest(searchUrl, HttpAccept.Json);
@@ -172,13 +171,11 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly BeyondHDSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly string _baseUrl;
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
_baseUrl = baseUrl;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
@@ -190,11 +187,11 @@ namespace NzbDrone.Core.Indexers.Definitions
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
// TODO Have BHD fix their API response content type so we can proper check here
// if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
// {
// throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
// }
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerResponse.HttpResponse);
foreach (var row in jsonResponse.Resource.Results)
@@ -217,10 +214,10 @@ namespace NzbDrone.Core.Indexers.Definitions
ImdbId = ParseUtil.GetImdbID(row.ImdbId).GetValueOrDefault(),
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.TmdbId.Split("/")[1]),
Peers = row.Leechers + row.Seeders,
DownloadVolumeFactor = row.Freeleech ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 172800, // 48 hours
MinimumSeedTime = 172800, // 120 hours
};
torrentInfos.Add(release);
@@ -242,7 +239,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class BeyondHDSettings : IProviderConfig
public class BeyondHDSettings : IIndexerSettings
{
private static readonly BeyondHDSettingsValidator Validator = new BeyondHDSettingsValidator();
@@ -250,12 +247,18 @@ namespace NzbDrone.Core.Indexers.Definitions
{
}
[FieldDefinition(1, Label = "API Key", HelpText = "API Key from Site", Privacy = PrivacyLevel.ApiKey)]
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in My Security => API Key)", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(2, Label = "RSS Key", HelpText = "RSS Key from Site", Privacy = PrivacyLevel.ApiKey)]
[FieldDefinition(3, Label = "RSS Key", HelpText = "RSS Key from the Site (Found in My Security => RSS Key)", Privacy = PrivacyLevel.ApiKey)]
public string RssKey { get; set; }
[FieldDefinition(4)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
@@ -300,5 +303,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public bool Promo25 { get; set; }
public bool Promo50 { get; set; }
public bool Promo75 { get; set; }
public bool Limited { get; set; }
}
}

View File

@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Indexers.Definitions
public class Blutopia : Unit3dBase
{
public override string Name => "Blutopia";
public override string BaseUrl => "https://blutopia.xyz/";
public override string[] IndexerUrls => new string[] { "https://blutopia.xyz/" };
public override string Description => "Blutopia (BLU) is a Private Torrent Tracker for HD MOVIES / TV";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public Blutopia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)

View File

@@ -17,7 +17,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override int PageSize => 100;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override string BaseUrl => "http://api.broadcasthe.net/";
public override string[] IndexerUrls => new string[] { "http://api.broadcasthe.net/" };
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
public BroadcastheNet(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
@@ -26,7 +27,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public override IIndexerRequestGenerator GetRequestGenerator()
{
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, BaseUrl = BaseUrl, Capabilities = Capabilities };
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, Capabilities = Capabilities };
var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id);
if (releaseInfo != null)

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