Compare commits

...

92 Commits

Author SHA1 Message Date
Bogdan
6ec761c217 Fixed: (Cardigann) Change UseBeforeResponse to Usebeforeresponse 2023-02-23 02:13:41 +02:00
Bogdan
b85679de56 Fixed: Filter releases with null description 2023-02-23 01:29:56 +02:00
Bogdan
71775b97a3 Fixed: (Rarbg) Check for rate limits before parsing token errors 2023-02-22 09:30:22 +02:00
bakerboy448
5bb3dbfbf5 Fixed: (Rarbg) Change app_id per site request 2023-02-21 23:39:30 +02:00
Bogdan
b608a7a904 Fixed: (FunFile) Change download url 2023-02-21 23:32:50 +02:00
Bogdan
4ad992f76a Fixed: (UI) Replace api. only if it's a subdomain 2023-02-21 22:59:08 +02:00
Bogdan
95497480a2 Fixed: (GreatPosterWall) Remove cookies only if redirected to login.php 2023-02-21 02:42:26 +02:00
Qstick
cc57866ab0 New: Filter releases by search criteria
Co-Authored-By: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-02-20 18:41:39 -06:00
Qstick
dbc4989a95 Fixed: (IndexerSearch) Update isRss logic for new properties 2023-02-20 18:41:39 -06:00
Bogdan
af4961e3e6 Fixed: (Rarbg) update cats 2023-02-21 02:38:29 +02:00
Bogdan
0ec54906c6 Fixed: (Caridgann) Custom headers in login and download blocks 2023-02-21 02:37:49 +02:00
Qstick
35f85fc986 More update tests 2023-02-19 23:40:53 -06:00
Qstick
0aedafb278 Fix update tests 2023-02-19 23:09:35 -06:00
Qstick
54dce448a8 Added react-hooks lint rules
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-02-19 23:09:35 -06:00
Servarr
3c915002c6 Automated API Docs update 2023-02-19 19:41:55 -06:00
Qstick
e32f8f4330 Remove unused tinytwitter library 2023-02-19 19:26:03 -06:00
Qstick
5abb5ada49 New: Ping Endpoint 2023-02-19 19:23:26 -06:00
Qstick
6579385110 Cleanup multi-platform code 2023-02-19 19:23:05 -06:00
Mark McDowall
1c6e5543df New: Return static response to requests while app is starting 2023-02-19 19:06:13 -06:00
Qstick
85737aacbe Bump version to 1.3.0 2023-02-19 18:14:12 -06:00
Servarr
30c3aedeb1 Automated API Docs update 2023-02-19 17:23:52 -06:00
Qstick
1640980e2b New: OnGrab Notifications 2023-02-19 17:16:05 -06:00
Bogdan
99bc56efb6 Fixed: (Indexers) Rate limit for download and auth 2023-02-19 18:54:17 +02:00
bakerboy448
04276eb587 Fixed: (Rarbg) Updated app_id per site request (#1447) 2023-02-19 18:23:18 +02:00
Bogdan
34c560fd3a Fixed: (CardigannBase) Remedy for casting strings to booleans 2023-02-19 17:07:25 +02:00
Bogdan
caa8bb05a7 Fixed: (Newznab API) Response with StatusCode 429 when limits are reached 2023-02-19 10:43:26 +02:00
Qstick
773e8ff1f4 Bump version to 1.2.2 2023-02-19 00:15:54 -06:00
Qstick
0984976378 Bump DryIoc, YamlDotNet, AngleSharp 2023-02-18 21:12:08 -06:00
Qstick
fcb3c96455 Call async methods when in an async method 2023-02-18 15:03:35 -06:00
Qstick
acf7a425b5 Add global analyzer config 2023-02-18 15:03:35 -06:00
Qstick
da898fe958 Remove Non-Failing Rules 2023-02-18 15:03:35 -06:00
Qstick
5bb3ea0806 Remove unnecessary assignments to default type value
The .NET runtime initializes all fields of reference types to their default values before running the constructor. In most cases, explicitly initializing a field to its default value in a constructor is redundant, adding maintenance costs and potentially degrading performance
2023-02-18 15:03:35 -06:00
Qstick
b41cb80e33 Use const where appropriate
The value of a const field is computed at compile time and stored in the metadata, which improves run-time performance when it is compared to a static readonly field.
2023-02-18 15:03:35 -06:00
Qstick
a39341be4b Enable all analyzers to default back to our rules 2023-02-18 15:03:35 -06:00
Weblate
27b3d8618a Translated using Weblate (Ukrainian)
Currently translated at 73.3% (349 of 476 strings)

Translated using Weblate (Slovak)

Currently translated at 22.6% (108 of 476 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 23.5% (112 of 476 strings)

Translated using Weblate (Catalan)

Currently translated at 74.5% (355 of 476 strings)

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

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (Arabic)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (476 of 476 strings)

Translated using Weblate (Vietnamese)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Turkish)

Currently translated at 71.4% (340 of 476 strings)

Translated using Weblate (Thai)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Swedish)

Currently translated at 86.3% (411 of 476 strings)

Translated using Weblate (Russian)

Currently translated at 76.4% (364 of 476 strings)

Translated using Weblate (Romanian)

Currently translated at 72.0% (343 of 476 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.2% (377 of 476 strings)

Translated using Weblate (Polish)

Currently translated at 74.1% (353 of 476 strings)

Translated using Weblate (Dutch)

Currently translated at 88.4% (421 of 476 strings)

Translated using Weblate (Korean)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Japanese)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (473 of 476 strings)

Translated using Weblate (Icelandic)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (Hindi)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Hebrew)

Currently translated at 81.3% (387 of 476 strings)

Translated using Weblate (French)

Currently translated at 98.5% (469 of 476 strings)

Translated using Weblate (Finnish)

Currently translated at 99.1% (472 of 476 strings)

Translated using Weblate (Spanish)

Currently translated at 77.9% (371 of 476 strings)

Translated using Weblate (Greek)

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (German)

Currently translated at 98.1% (467 of 476 strings)

Translated using Weblate (Danish)

Currently translated at 74.5% (355 of 476 strings)

Translated using Weblate (Czech)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Spanish)

Currently translated at 77.9% (371 of 476 strings)

Translated using Weblate (Arabic)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Bulgarian)

Currently translated at 67.0% (319 of 476 strings)

Translated using Weblate (Spanish)

Currently translated at 77.9% (371 of 476 strings)

Translated using Weblate (Croatian)

Currently translated at 18.2% (87 of 476 strings)

Translated using Weblate (Croatian)

Currently translated at 18.2% (87 of 476 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.3% (349 of 476 strings)

Translated using Weblate (Slovak)

Currently translated at 22.6% (108 of 476 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 23.5% (112 of 476 strings)

Translated using Weblate (Catalan)

Currently translated at 74.5% (355 of 476 strings)

Translated using Weblate (Catalan)

Currently translated at 74.5% (355 of 476 strings)

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

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (Arabic)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (476 of 476 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (476 of 476 strings)

Translated using Weblate (Vietnamese)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Turkish)

Currently translated at 71.4% (340 of 476 strings)

Translated using Weblate (Thai)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Swedish)

Currently translated at 86.3% (411 of 476 strings)

Translated using Weblate (Russian)

Currently translated at 76.4% (364 of 476 strings)

Translated using Weblate (Romanian)

Currently translated at 72.0% (343 of 476 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.2% (377 of 476 strings)

Translated using Weblate (Polish)

Currently translated at 74.1% (353 of 476 strings)

Translated using Weblate (Dutch)

Currently translated at 88.4% (421 of 476 strings)

Translated using Weblate (Korean)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Japanese)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (473 of 476 strings)

Translated using Weblate (Icelandic)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (Hindi)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Hebrew)

Currently translated at 81.3% (387 of 476 strings)

Translated using Weblate (French)

Currently translated at 98.5% (469 of 476 strings)

Translated using Weblate (Finnish)

Currently translated at 99.1% (472 of 476 strings)

Translated using Weblate (Spanish)

Currently translated at 77.7% (370 of 476 strings)

Translated using Weblate (Greek)

Currently translated at 99.5% (474 of 476 strings)

Translated using Weblate (German)

Currently translated at 98.1% (467 of 476 strings)

Translated using Weblate (Danish)

Currently translated at 74.5% (355 of 476 strings)

Translated using Weblate (Czech)

Currently translated at 71.6% (341 of 476 strings)

Translated using Weblate (Bulgarian)

Currently translated at 67.0% (319 of 476 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: TheHrle <Hpranjkovic@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: fiego14 <alvaross_96@hotmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-02-18 14:39:38 -06:00
Bogdan
550b9b58df Fixed: (TorrentIndexerBase) Validate downloaded torrent data 2023-02-18 22:13:33 +02:00
Bogdan
035ad33b72 Fixed: (Nebulance) Prevent redirect to login page when downloading torrent files 2023-02-18 21:20:17 +02:00
Qstick
85f8e0c451 Update MagnetLinkBuilder public trackers
8ba874e69d
2023-02-18 11:49:34 -06:00
bakerboy448
ea2061a7d3 fixup!
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-02-18 19:24:47 +02:00
Bakerboy448
ea6d01a49b Fixed: (RarBG) Handle HTTP 200 Rate Limiting False Positive
Fixes: #1277
Finishes: #1169
Related: #1380

partially reverts 5cc044aa8f
2023-02-18 19:24:47 +02:00
Bogdan
252cd97e35 Fixed: (SpeedAppBase) Add pagination 2023-02-18 19:08:02 +02:00
Bogdan
a8ea05af07 Fixed: (Nebulance) Add SupportsRedirect since their API is stateless 2023-02-18 19:01:37 +02:00
Bogdan
24d6a0cb06 Fixed: (UI) Remedy for external link regression 2023-02-18 19:01:23 +02:00
Bakerboy448
8e1771b5a9 Fixed: Improved Indexer HTTP Validation Failure Messaging 2023-02-17 11:23:59 +02:00
Bogdan
d767a82e84 Fixed: (RuTracker) Add "Use Magnet Links" and "Add RUS to title" options 2023-02-17 07:12:57 +02:00
Bogdan
76bfd29f23 New: Add UniOtaku 2023-02-17 07:12:21 +02:00
Bogdan
c923982711 New: (AudioBookBay) Migrate to C# 2023-02-17 07:11:28 +02:00
Bogdan
f03a64f9ac Fixed: (Shazbat) Fix Guid 2023-02-15 06:09:06 +02:00
Bogdan
e713e58e83 Fixed: (ImmortalSeed) Set RateLimit to 5 2023-02-15 06:08:47 +02:00
Bogdan
4fb5d3432b Fixed: (FileList) Switch to Basic Auth 2023-02-13 02:28:02 +02:00
Qstick
a31b107a90 Fix some UI translated strings 2023-02-12 18:22:43 -06:00
Qstick
f91ffb8328 New: (Localization) 7 New Languages 2023-02-12 17:57:22 -06:00
Weblate
a3ba070296 Added translation using Weblate (Tamil)
Added translation using Weblate (Indonesian)

Added translation using Weblate (Estonian)

Added translation using Weblate (Serbian)

Added translation using Weblate (Croatian)

Added translation using Weblate (Bosnian)

Added translation using Weblate (Spanish (Mexico))

Co-authored-by: Weblate <noreply@weblate.org>
2023-02-12 17:38:07 -06:00
Qstick
bccb0bd5c8 Bump version to 1.2.1 2023-02-11 13:25:32 -06:00
Weblate
4517f271c4 Translated using Weblate (Greek)
Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.4% (346 of 471 strings)

Translated using Weblate (Russian)

Currently translated at 76.6% (361 of 471 strings)

Translated using Weblate (Dutch)

Currently translated at 88.7% (418 of 471 strings)

Translated using Weblate (Hebrew)

Currently translated at 81.5% (384 of 471 strings)

Translated using Weblate (French)

Currently translated at 98.9% (466 of 471 strings)

Translated using Weblate (Greek)

Currently translated at 75.5% (356 of 471 strings)

Translated using Weblate (Danish)

Currently translated at 74.7% (352 of 471 strings)

Translated using Weblate (French)

Currently translated at 97.0% (457 of 471 strings)

Translated using Weblate (French)

Currently translated at 97.0% (457 of 471 strings)

Translated using Weblate (French)

Currently translated at 97.0% (457 of 471 strings)

Translated using Weblate (French)

Currently translated at 96.1% (453 of 471 strings)

Translated using Weblate (French)

Currently translated at 96.1% (453 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: KevoM <lilmarsu@gmail.com>
Co-authored-by: Vasilis Ieropoulos <kirav96@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: louismaxx <lmdupouy@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translation: Servarr/Prowlarr
2023-02-11 11:28:14 -06:00
Qstick
2ae2a0b184 Delete azuresync.yml 2023-02-11 10:35:07 -06:00
Bogdan
b5e43e7a1a Fixed: (Cardigann) Show redirect url when the response has errors 2023-02-11 08:28:38 +02:00
Bogdan
3a52048dc2 Fixed: (UI) Check for non-array indexerUrls 2023-02-11 08:27:16 +02:00
Bogdan
8b898733ab Fixed: (RuTracker/Toloka) Clean title 2023-02-10 06:46:02 +02:00
Bogdan
f99a2e1164 Fixed: Standardize dashes/single quotes in search term, ignore artist if "VA" 2023-02-10 06:44:39 +02:00
Bogdan
306209fcc2 Fixed: Simplify DateTime alteration 2023-02-09 13:13:16 +02:00
Bogdan
5d09c2b5fa Fixed: (Shazbat) Simplify conditions for CheckIfLoginNeeded 2023-02-09 12:48:39 +02:00
Bogdan
41a9d2d732 New: Add Shazbat 2023-02-09 07:05:29 +02:00
Bogdan
49b120ba55 Revert "Fixed: (Redacted/Orpheus/Libble/SecretCinema) Add SupportsRawSearch"
This reverts commit c46b7c5e4b.
2023-02-08 06:28:05 +02:00
Qstick
a88fc34a78 Fixed: Settings fail to save for some auth setups 2023-02-06 23:12:20 -06:00
Bogdan
c46b7c5e4b Fixed: (Redacted/Orpheus/Libble/SecretCinema) Add SupportsRawSearch 2023-02-07 07:00:27 +02:00
Bogdan
94c45541ae Fixed: (Anidub/Animedia) Use rate limit in sub-requests 2023-02-07 06:59:48 +02:00
Bogdan
f8082047a5 Fixed: (HttpIndexerBase) Catch HttpRequestException/TaskCanceledException 2023-02-05 20:00:10 +02:00
Qstick
011fd57f7d Fixed: Handle null IEnumerable field values in SchemaBuilder 2023-02-05 10:37:35 -06:00
Bogdan
6c35c3fc6f Fixed: (ImmortalSeed/XSpeeds) Sitewide Freeleech 2023-02-05 01:34:36 +02:00
Qstick
5da02c49eb Bump version to 1.2.0 2023-02-04 15:07:01 -06:00
Bogdan
1a339b9ab2 Fixed: (ImmortalSeed) Add sorting to skip the sticky results 2023-02-04 07:39:17 +02:00
Bogdan
94edd7538e Fixed: (GreatPosterWall) Remove JsonProperty 2023-02-04 07:08:58 +02:00
Bogdan
9b2274805e Fixed: (GreatPosterWall) Remove special characters from titles 2023-02-04 07:08:58 +02:00
Bogdan
dbf86efb0a Fixed: (ExecuteAuth) Request timeout of 15s by default, if not set otherwise 2023-02-04 05:52:33 +02:00
Weblate
529fbfd9bd Translated using Weblate (Hebrew)
Currently translated at 80.8% (381 of 471 strings)

Translated using Weblate (Greek)

Currently translated at 73.8% (348 of 471 strings)

Translated using Weblate (Danish)

Currently translated at 74.0% (349 of 471 strings)

Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Vasilis Ieropoulos <kirav96@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hhjuhl <hans@kopula.dk>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translation: Servarr/Prowlarr
2023-02-03 21:49:54 -06:00
Bogdan
0ed5bfe0d0 Fixed: (AroLol) Make login possible without 2FA 2023-02-04 05:46:58 +02:00
Bogdan
6a43eb0031 Fixed: (HDBits) Change TVDB search for daily shows, append slash to IndexerUrl 2023-02-04 05:45:38 +02:00
Bogdan
a12001a5ef Fixed: (XSpeeds) Category filtering if single, add sorting to skip sticky 2023-02-04 05:42:11 +02:00
Qstick
b57014762d Fixed: (RuTracker) Update categories 2023-02-01 22:36:37 -06:00
Bogdan
a51a8bf921 Fixed: (GreatPosterWall) Parse categories based on resolution 2023-02-02 06:18:51 +02:00
Martin Häger
e8dc5b3206 Serve plain text files (e.g. logs) as UTF-8. 2023-02-01 22:17:30 -06:00
Bogdan
d4f22f3596 Fixed: (assorted) Use GetArgumentFromQueryString and other minor fixes 2023-02-02 06:09:13 +02:00
Bogdan
b6018a4cd7 Fixed: (norbits) Refactor parsing 2023-02-02 06:06:20 +02:00
Bogdan
ec389987df Fixed: (pornolab) Improvements generator/parsing 2023-02-02 06:04:02 +02:00
Bogdan
6b62504916 Fixed: (PreToMe) Improved parsing, login and settings to extend UserPassTorrentBaseSettings 2023-02-02 06:02:31 +02:00
Bogdan
626d777d3c Fixed: (HttpIndexerBase) Add IndexerAuthException to logs 2023-02-02 06:02:17 +02:00
Bogdan
234707b291 Fixed: (SpeedCD) Fix wildcard when using air date 2023-02-02 06:02:01 +02:00
Bogdan
15734ca0da Fixed: (Libble) Minor improvements 2023-02-02 05:28:01 +02:00
Bogdan
19913e5b01 Fixed: (CloudFlareDetection) Check for DDoS-Guard case-insensitive 2023-02-02 05:20:29 +02:00
Qstick
156f6505be Bump version to 1.1.3 2023-01-30 21:21:33 -06:00
Bogdan
e383287972 New: Add FunFile 2023-01-31 03:05:00 +02:00
237 changed files with 6365 additions and 3715 deletions

View File

@@ -117,7 +117,6 @@ dotnet_diagnostic.CA1003.severity = suggestion
dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion
dotnet_diagnostic.CA1012.severity = suggestion
dotnet_diagnostic.CA1014.severity = suggestion
dotnet_diagnostic.CA1016.severity = suggestion
dotnet_diagnostic.CA1017.severity = suggestion
dotnet_diagnostic.CA1018.severity = suggestion
@@ -163,6 +162,7 @@ dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1419.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion
@@ -178,9 +178,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
@@ -192,13 +189,14 @@ dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1835.severity = suggestion
dotnet_diagnostic.CA1845.severity = suggestion
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA1849.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion
@@ -229,6 +227,7 @@ dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA2254.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion
@@ -255,6 +254,7 @@ dotnet_diagnostic.CA5385.severity = suggestion
dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion
dotnet_diagnostic.CA5401.severity = suggestion
dotnet_diagnostic.SYSLIB0014.severity = none

View File

@@ -1,41 +0,0 @@
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

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.1.2'
majorVersion: '1.3.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -39,6 +39,7 @@ module.exports = {
plugins: [
'filenames',
'react',
'react-hooks',
'simple-import-sort',
'import'
],
@@ -308,7 +309,9 @@ module.exports = {
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2
'react/jsx-wrap-multilines': 2,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
},
overrides: [
{

View File

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import React, { Fragment, useEffect } from 'react';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
@@ -19,7 +19,8 @@ function createMapStateToProps() {
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
function updateCSSVariables() {
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
@@ -31,12 +32,12 @@ function ApplyTheme({ theme, children }) {
arrayOfVariableValues[index]
);
});
}
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [theme]);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}

View File

@@ -50,7 +50,7 @@ function CustomFiltersModalContent(props) {
<div className={styles.addButtonContainer}>
<Button onPress={onAddCustomFilter}>
Add Custom Filter
{translate('AddCustomFilter')}
</Button>
</div>
</ModalBody>

View File

@@ -30,10 +30,10 @@ function ConfirmModal(props) {
useEffect(() => {
if (isOpen) {
bindShortcut('enter', onConfirm);
} else {
unbindShortcut('enter', onConfirm);
return () => unbindShortcut('enter', onConfirm);
}
}, [onConfirm]);
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return (
<Modal

View File

@@ -61,15 +61,15 @@ class TagsModalContent extends Component {
} = this.state;
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' }
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') }
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Tags
{translate('Tags')}
</ModalHeader>
<ModalBody>

View File

@@ -26,7 +26,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Status
{translate('Status')}
</SortMenuItem>
<SortMenuItem
@@ -62,7 +62,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{'Priority'}
{translate('Priority')}
</SortMenuItem>
<SortMenuItem
@@ -71,7 +71,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{'Protocol'}
{translate('Protocol')}
</SortMenuItem>
<SortMenuItem
@@ -80,7 +80,7 @@ function IndexerIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
{'Privacy'}
{translate('Privacy')}
</SortMenuItem>
</MenuContent>
</SortMenu>

View File

@@ -97,7 +97,7 @@ class IndexerIndexRow extends Component {
isIndexerInfoModalOpen
} = this.state;
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? indexerUrls[0];
const baseUrl = fields.find((field) => field.name === 'baseUrl')?.value ?? (Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
return (
<>
@@ -248,12 +248,12 @@ class IndexerIndexRow extends Component {
/>
{
indexerUrls ?
baseUrl ?
<IconButton
className={styles.externalLink}
name={icons.EXTERNAL_LINK}
title={translate('Website')}
to={baseUrl.replace('api.', '')}
to={baseUrl.replace(/(:\/\/)api\./, '$1')}
/> : null
}

View File

@@ -71,6 +71,19 @@ class SearchIndexRow extends Component {
});
};
onSavePress = () => {
const {
downloadUrl,
fileName,
onSavePress
} = this.props;
onSavePress({
downloadUrl,
fileName
});
};
//
// Render
@@ -85,7 +98,6 @@ class SearchIndexRow extends Component {
publishDate,
title,
infoUrl,
downloadUrl,
indexer,
size,
files,
@@ -300,7 +312,7 @@ class SearchIndexRow extends Component {
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl}
onPress={this.onSavePress}
/>
</VirtualTableRowCell>
);
@@ -323,6 +335,7 @@ SearchIndexRow.propTypes = {
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
fileName: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
@@ -335,6 +348,7 @@ SearchIndexRow.propTypes = {
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onGrabPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,

View File

@@ -51,7 +51,8 @@ class SearchIndexTable extends Component {
timeFormat,
selectedState,
onSelectedChange,
onGrabPress
onGrabPress,
onSavePress
} = this.props;
const release = items[rowIndex];
@@ -71,6 +72,7 @@ class SearchIndexTable extends Component {
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
onSavePress={onSavePress}
/>
</VirtualTableRow>
);
@@ -134,6 +136,7 @@ SearchIndexTable.propTypes = {
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabRelease, setReleasesSort } from 'Store/Actions/releaseActions';
import { grabRelease, saveRelease, setReleasesSort } from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexTable from './SearchIndexTable';
@@ -25,6 +25,9 @@ function createMapDispatchToProps(dispatch, props) {
},
onGrabPress(payload) {
dispatch(grabRelease(payload));
},
onSavePress(payload) {
dispatch(saveRelease(payload));
}
};
}

View File

@@ -28,7 +28,7 @@ class AddApplicationModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add Application
{translate('AddApplication')}
</ModalHeader>
<ModalBody>

View File

@@ -71,14 +71,14 @@ class Application extends Component {
{
syncLevel === 'addOnly' &&
<Label kind={kinds.WARNING}>
Add and Remove Only
{translate('AddRemoveOnly')}
</Label>
}
{
syncLevel === 'fullSync' &&
<Label kind={kinds.SUCCESS}>
Full Sync
{translate('FullSync')}
</Label>
}
@@ -88,7 +88,7 @@ class Application extends Component {
kind={kinds.DISABLED}
outline={true}
>
Disabled
{translate('Disabled')}
</Label>
}

View File

@@ -106,7 +106,7 @@ class IndexerProxy extends Component {
kind={kinds.DISABLED}
outline={true}
>
Disabled
{translate('Disabled')}
</Label> :
null
}

View File

@@ -56,17 +56,9 @@ class Notification extends Component {
id,
name,
onGrab,
onDownload,
onUpgrade,
onRename,
onDelete,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnRename,
supportsOnDelete,
supportsOnHealthIssue,
supportsOnApplicationUpdate
} = this.props;
@@ -88,34 +80,6 @@ class Notification extends Component {
</Label>
}
{
supportsOnDelete && onDelete &&
<Label kind={kinds.SUCCESS}>
{translate('OnDelete')}
</Label>
}
{
supportsOnDownload && onDownload &&
<Label kind={kinds.SUCCESS}>
{translate('OnImport')}
</Label>
}
{
supportsOnUpgrade && onDownload && onUpgrade &&
<Label kind={kinds.SUCCESS}>
{translate('OnUpgrade')}
</Label>
}
{
supportsOnRename && onRename &&
<Label kind={kinds.SUCCESS}>
{translate('OnRename')}
</Label>
}
{
supportsOnHealthIssue && onHealthIssue &&
<Label kind={kinds.SUCCESS}>
@@ -132,7 +96,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete && !onApplicationUpdate ?
!onGrab && !onHealthIssue && !onApplicationUpdate ?
<Label
kind={kinds.DISABLED}
outline={true}
@@ -167,17 +131,9 @@ Notification.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
onGrab: PropTypes.bool.isRequired,
onDownload: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onDelete: PropTypes.bool.isRequired,
onHealthIssue: PropTypes.bool.isRequired,
onApplicationUpdate: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnDelete: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired,
supportsOnHealthIssue: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired

View File

@@ -15,8 +15,11 @@ function NotificationEventItems(props) {
} = props;
const {
onGrab,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
includeManualGrabs,
supportsOnHealthIssue,
includeHealthWarnings,
supportsOnApplicationUpdate
@@ -31,6 +34,31 @@ function NotificationEventItems(props) {
link="https://wiki.servarr.com/prowlarr/settings#connections"
/>
<div className={styles.events}>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText={translate('OnGrabHelpText')}
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</div>
{
onGrab.value &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="includeManualGrabs"
helpText={translate('IncludeManualGrabsHelpText')}
isDisabled={!supportsOnGrab.value}
{...includeManualGrabs}
onChange={onInputChange}
/>
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}

View File

@@ -20,7 +20,7 @@ function PendingChangesModal(props) {
useEffect(() => {
bindShortcut('enter', onConfirm);
}, [onConfirm]);
}, [bindShortcut, onConfirm]);
return (
<Modal

View File

@@ -103,9 +103,6 @@ export default {
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
return selectedSchema;

View File

@@ -54,7 +54,7 @@ export const defaultState = {
},
{
name: 'grabTitle',
label: translate('Grab Title'),
label: translate('GrabTitle'),
isSortable: false,
isVisible: false
},
@@ -78,7 +78,7 @@ export const defaultState = {
},
{
name: 'elapsedTime',
label: translate('Elapsed Time'),
label: translate('ElapsedTime'),
isSortable: false,
isVisible: true
},

View File

@@ -1,3 +1,4 @@
import $ from 'jquery';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
@@ -229,6 +230,7 @@ export const CANCEL_FETCH_RELEASES = 'releases/cancelFetchReleases';
export const SET_RELEASES_SORT = 'releases/setReleasesSort';
export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease';
export const SAVE_RELEASE = 'releases/saveRelease';
export const BULK_GRAB_RELEASES = 'release/bulkGrabReleases';
export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
@@ -243,6 +245,7 @@ export const cancelFetchReleases = createThunk(CANCEL_FETCH_RELEASES);
export const setReleasesSort = createAction(SET_RELEASES_SORT);
export const clearReleases = createAction(CLEAR_RELEASES);
export const grabRelease = createThunk(GRAB_RELEASE);
export const saveRelease = createThunk(SAVE_RELEASE);
export const bulkGrabReleases = createThunk(BULK_GRAB_RELEASES);
export const updateRelease = createAction(UPDATE_RELEASE);
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
@@ -304,6 +307,32 @@ export const actionHandlers = handleThunks({
});
},
[SAVE_RELEASE]: function(getState, payload, dispatch) {
const link = payload.downloadUrl;
const file = payload.fileName;
$.ajax({
url: link,
method: 'GET',
headers: {
'X-Prowlarr-Client': true
},
xhrFields: {
responseType: 'blob'
},
success: function(data) {
const a = document.createElement('a');
const url = window.URL.createObjectURL(data);
a.href = url;
a.download = file;
document.body.append(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
});
},
[BULK_GRAB_RELEASES]: function(getState, payload, dispatch) {
dispatch(set({
section,

View File

@@ -25,7 +25,7 @@ const columns = [
},
{
name: 'size',
label: 'Size',
label: translate('Size'),
isVisible: true
},
{

View File

@@ -113,7 +113,7 @@ class Updates extends Component {
/>
<div className={styles.message}>
The latest version of Prowlarr is already installed
{translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])}
</div>
{

View File

@@ -16,6 +16,11 @@ function addApiKey(ajaxOptions) {
ajaxOptions.headers['X-Api-Key'] = window.Prowlarr.apiKey;
}
function addUIHeader(ajaxOptions) {
ajaxOptions.headers = ajaxOptions.headers || {};
ajaxOptions.headers['X-Prowlarr-Client'] = true;
}
function addContentType(ajaxOptions) {
if (
ajaxOptions.contentType == null &&
@@ -42,6 +47,7 @@ export default function createAjaxRequest(originalAjaxOptions) {
if (isRelative(ajaxOptions)) {
addRootUrl(ajaxOptions);
addApiKey(ajaxOptions);
addUIHeader(ajaxOptions);
addContentType(ajaxOptions);
}

View File

@@ -105,6 +105,7 @@
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "8.0.0",
"esprint": "3.6.0",
"file-loader": "6.2.0",

3
src/.globalconfig Normal file
View File

@@ -0,0 +1,3 @@
is_global = true
dotnet_diagnostic.CA1014.severity = none

View File

@@ -1,6 +1,7 @@
<Project>
<!-- Common to all Prowlarr Projects -->
<PropertyGroup>
<AnalysisLevel>6.0-all</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<PlatformTarget>AnyCPU</PlatformTarget>

View File

@@ -278,7 +278,7 @@ namespace NzbDrone.Common.Test
[Test]
public void GetUpdateClientExePath()
{
GetIAppDirectoryInfo().GetUpdateClientExePath(PlatformType.DotNet).Should().BeEquivalentTo(@"C:\Temp\prowlarr_update\Prowlarr.Update.exe".AsOsAgnostic());
GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\prowlarr_update\Prowlarr.Update".AsOsAgnostic().ProcessNameToExe());
}
[Test]

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Common.EnvironmentInfo
bool IsAdmin { get; }
bool IsWindowsService { get; }
bool IsWindowsTray { get; }
bool IsStarting { get; set; }
bool IsExiting { get; set; }
bool IsTray { get; }
RuntimeMode Mode { get; }

View File

@@ -2,13 +2,6 @@ using System;
namespace NzbDrone.Common.EnvironmentInfo
{
public enum PlatformType
{
DotNet = 0,
Mono = 1,
NetCore = 2
}
public interface IPlatformInfo
{
Version Version { get; }
@@ -16,36 +9,18 @@ namespace NzbDrone.Common.EnvironmentInfo
public class PlatformInfo : IPlatformInfo
{
private static PlatformType _platform;
private static Version _version;
static PlatformInfo()
{
_platform = PlatformType.NetCore;
_version = Environment.Version;
}
public static PlatformType Platform => _platform;
public static bool IsMono => Platform == PlatformType.Mono;
public static bool IsDotNet => Platform == PlatformType.DotNet;
public static bool IsNetCore => Platform == PlatformType.NetCore;
public static string PlatformName
{
get
{
if (IsDotNet)
{
return ".NET";
}
else if (IsMono)
{
return "Mono";
}
else
{
return ".NET Core";
}
return ".NET";
}
}

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Common.EnvironmentInfo
_logger = logger;
IsWindowsService = hostLifetime is WindowsServiceLifetime;
IsStarting = true;
// net6.0 will return Radarr.dll for entry assembly, we need the actual
// executable name (Radarr on linux). On mono this will return the location of
@@ -82,6 +83,7 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool IsWindowsService { get; private set; }
public bool IsStarting { get; set; }
public bool IsExiting { get; set; }
public bool IsTray
{

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Common.Extensions
var info = new FileInfo(path.Trim());
//UNC
// UNC
if (OsInfo.IsWindows && info.FullName.StartsWith(@"\\"))
{
return info.FullName.TrimEnd('/', '\\', ' ');
@@ -166,7 +166,7 @@ namespace NzbDrone.Common.Extensions
var parentDirInfo = dirInfo.Parent;
if (parentDirInfo == null)
{
//Drive letter
// Drive letter
return dirInfo.Name.ToUpper();
}
@@ -238,9 +238,9 @@ namespace NzbDrone.Common.Extensions
return null;
}
public static string ProcessNameToExe(this string processName, PlatformType runtime)
public static string ProcessNameToExe(this string processName)
{
if (OsInfo.IsWindows || runtime != PlatformType.NetCore)
if (OsInfo.IsWindows)
{
processName += ".exe";
}
@@ -248,11 +248,6 @@ namespace NzbDrone.Common.Extensions
return processName;
}
public static string ProcessNameToExe(this string processName)
{
return processName.ProcessNameToExe(PlatformInfo.Platform);
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{
return appFolderInfo.AppDataFolder;
@@ -318,9 +313,9 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetUpdatePackageFolder(appFolderInfo), UPDATE_CLIENT_FOLDER_NAME);
}
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo, PlatformType runtime)
public static string GetUpdateClientExePath(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe(runtime);
return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_CLIENT_EXE_NAME).ProcessNameToExe();
}
public static string GetDatabase(this IAppFolderInfo appFolderInfo)

View File

@@ -131,7 +131,7 @@ namespace NzbDrone.Common.Extensions
public static string WrapInQuotes(this string text)
{
if (!text.Contains(" "))
if (!text.Contains(' '))
{
return text;
}
@@ -255,7 +255,7 @@ namespace NzbDrone.Common.Extensions
public static string ToUrlHost(this string input)
{
return input.Contains(":") ? $"[{input}]" : input;
return input.Contains(':') ? $"[{input}]" : input;
}
}
}

View File

@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Http.Dispatchers
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
else
{

View File

@@ -11,15 +11,13 @@ namespace NzbDrone.Common.Http
{
if (response.Headers.ContainsKey("Retry-After"))
{
var retryAfter = response.Headers["Retry-After"].ToString();
int seconds;
DateTime date;
var retryAfter = response.Headers["Retry-After"];
if (int.TryParse(retryAfter, out seconds))
if (int.TryParse(retryAfter, out var seconds))
{
RetryAfter = TimeSpan.FromSeconds(seconds);
}
else if (DateTime.TryParse(retryAfter, out date))
else if (DateTime.TryParse(retryAfter, out var date))
{
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
}

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NzbDrone.Common.Http
{

View File

@@ -226,7 +226,7 @@ namespace NzbDrone.Common.OAuth
#if WINRT
return CultureInfo.InvariantCulture.CompareInfo.Compare(left, right, CompareOptions.IgnoreCase) == 0;
#else
return string.Compare(left, right, StringComparison.InvariantCultureIgnoreCase) == 0;
return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
#endif
}

View File

@@ -4,7 +4,7 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />

View File

@@ -64,7 +64,7 @@ namespace NzbDrone.Common
var args = $"create {serviceName} " +
$"DisplayName= \"{serviceName}\" " +
$"binpath= \"{Process.GetCurrentProcess().MainModule.FileName}\" " +
$"binpath= \"{Environment.ProcessPath}\" " +
"start= auto " +
"depend= EventLog/Tcpip/http " +
"obj= \"NT AUTHORITY\\LocalService\"";

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -19,7 +19,7 @@ namespace NzbDrone.Common.TPL
private readonly int _maxDegreeOfParallelism;
/// <summary>Whether the scheduler is currently processing work items.</summary>
private int _delegatesQueuedOrRunning = 0;
private int _delegatesQueuedOrRunning;
/// <summary>
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the

View File

@@ -10,7 +10,7 @@ using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.HDBits;
using NzbDrone.Core.Indexers.Definitions.HDBits;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;

View File

@@ -5,7 +5,7 @@ using Newtonsoft.Json;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.HDBits;
using NzbDrone.Core.Indexers.Definitions.HDBits;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
@@ -14,11 +14,13 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
public class HDBitsRequestGeneratorFixture : CoreTest<HDBitsRequestGenerator>
{
private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchSeasonEpisodeCriteria;
private TvSearchCriteria _tvSearchDailyEpisodeCriteria;
[SetUp]
public void Setup()
{
Subject.Settings = new HDBitsSettings()
Subject.Settings = new HDBitsSettings
{
ApiKey = "abcd",
Username = "somename"
@@ -47,9 +49,25 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
_movieSearchCriteria = new MovieSearchCriteria
{
Categories = new int[] { 2000, 2010 },
Categories = new[] { 2000, 2010 },
ImdbId = "0076759"
};
_tvSearchSeasonEpisodeCriteria = new TvSearchCriteria
{
Categories = new[] { 5000, 5010 },
TvdbId = 392256,
Season = 1,
Episode = "3"
};
_tvSearchDailyEpisodeCriteria = new TvSearchCriteria
{
Categories = new[] { 5000, 5010 },
TvdbId = 289574,
Season = 2023,
Episode = "01/03"
};
}
[Test]
@@ -70,5 +88,49 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
query.Category.Should().HaveCount(1);
query.ImdbInfo.Id.Should().Be(imdbQuery);
}
[Test]
public void should_search_by_tvdbid_season_episode_if_supported()
{
var results = Subject.GetSearchRequests(_tvSearchSeasonEpisodeCriteria);
var tvdbQuery = _tvSearchSeasonEpisodeCriteria.TvdbId;
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var encoding = HttpHeader.GetEncodingFromContentType(page.HttpRequest.Headers.ContentType);
var body = encoding.GetString(page.HttpRequest.ContentData);
var query = JsonConvert.DeserializeObject<TorrentQuery>(body);
query.Category.Should().HaveCount(3);
query.TvdbInfo.Id.Should().Be(tvdbQuery);
query.Search.Should().BeNullOrWhiteSpace();
query.TvdbInfo.Season.Should().Be(1);
query.TvdbInfo.Episode.Should().Be("3");
}
[Test]
public void should_search_by_tvdbid_daily_episode_if_supported()
{
var results = Subject.GetSearchRequests(_tvSearchDailyEpisodeCriteria);
var tvdbQuery = _tvSearchDailyEpisodeCriteria.TvdbId;
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var encoding = HttpHeader.GetEncodingFromContentType(page.HttpRequest.Headers.ContentType);
var body = encoding.GetString(page.HttpRequest.ContentData);
var query = JsonConvert.DeserializeObject<TorrentQuery>(body);
query.Category.Should().HaveCount(3);
query.TvdbInfo.Id.Should().Be(tvdbQuery);
query.Search.Should().Be("2023-01-03");
query.TvdbInfo.Season.Should().BeNull();
query.TvdbInfo.Episode.Should().BeNull();
}
}
}

View File

@@ -9,7 +9,7 @@ using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Rarbg;
using NzbDrone.Core.Indexers.Definitions.Rarbg;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
@@ -23,14 +23,14 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "Rarbg",
Settings = new RarbgSettings()
};
Mocker.GetMock<IRarbgTokenProvider>()
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), Subject.RateLimit))
.Returns("validtoken");
}
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
torrentInfo.InfoUrl.Should().Be($"https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id={BuildInfo.AppName}");
torrentInfo.InfoUrl.Should().Be($"https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=rralworP_{BuildInfo.Version}");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
torrentInfo.Size.Should().Be(564198371);

View File

@@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
releaseInfo.InfoHash.Should().Be("(removed)");
releaseInfo.Seeders.Should().Be(3);
releaseInfo.Peers.Should().Be(3);
releaseInfo.Categories.Count().Should().Be(4);
releaseInfo.Categories.Count.Should().Be(4);
}
[Test]

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentValidation.Results;
using NUnit.Framework;
@@ -56,6 +55,11 @@ namespace NzbDrone.Core.Test.NotificationTests
{
TestLogger.Info("OnApplicationUpdate was called");
}
public override void OnGrab(GrabMessage message)
{
TestLogger.Info("OnGrab was called");
}
}
private class TestNotificationWithNoEvents : NotificationBase<TestSetting>
@@ -76,6 +80,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnHealthIssue.Should().BeTrue();
notification.SupportsOnApplicationUpdate.Should().BeTrue();
notification.SupportsOnGrab.Should().BeTrue();
}
[Test]
@@ -85,6 +90,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnHealthIssue.Should().BeFalse();
notification.SupportsOnApplicationUpdate.Should().BeFalse();
notification.SupportsOnGrab.Should().BeFalse();
}
}
}

View File

@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="12.3.1" />
<PackageReference Include="YamlDotNet" Version="13.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -40,12 +40,11 @@ namespace NzbDrone.Core.Test.UpdateTests
}
[Test]
[Ignore("TODO No Updates On Server")]
public void should_get_recent_updates()
{
const string branch = "develop";
UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(2, 0), null);
var recent = Subject.GetRecentUpdates(branch, new Version(1, 0), null);
var recentWithChanges = recent.Where(c => c.Changes != null);
recent.Should().NotBeEmpty();

View File

@@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.UpdateTests
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update".ProcessNameToExe()))))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.UpdateTests
public void should_return_with_warning_if_updater_doesnt_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update".ProcessNameToExe()))))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());

View File

@@ -19,14 +19,14 @@ namespace NzbDrone.Core.Authentication
public class UserService : IUserService
{
private const int ITERATIONS = 10000;
private const int SALT_SIZE = 128 / 8;
private const int NUMBER_OF_BYTES = 256 / 8;
private readonly IUserRepository _repo;
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
private static readonly int ITERATIONS = 10000;
private static readonly int SALT_SIZE = 128 / 8;
private static readonly int NUMBER_OF_BYTES = 256 / 8;
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{
_repo = repo;

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(029)]
public class add_on_grab_to_notifications : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Notifications").AddColumn("OnGrab").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("IncludeManualGrabs").AsBoolean().WithDefaultValue(false).NotNullable();
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
@@ -250,7 +250,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
}
Index = end + 1;
identifier.Append(Buffer.Substring(start, end - start));
identifier.Append(Buffer.AsSpan(start, end - start));
if (Buffer[Index] != escape)
{

View File

@@ -66,6 +66,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
.Ignore(x => x.ImplementationName)
.Ignore(i => i.SupportsOnGrab)
.Ignore(i => i.SupportsOnHealthIssue)
.Ignore(i => i.SupportsOnApplicationUpdate);

View File

@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
private readonly bool _requireConcreteValue;
private int _paramCount;
private bool _gotConcreteValue;
public WhereBuilderPostgres(Expression filter, bool requireConcreteValue, int seq)
{

View File

@@ -15,9 +15,9 @@ namespace NzbDrone.Core.Datastore
private const DbType EnumerableMultiParameter = (DbType)(-1);
private readonly string _paramNamePrefix;
private readonly bool _requireConcreteValue = false;
private int _paramCount = 0;
private bool _gotConcreteValue = false;
private readonly bool _requireConcreteValue;
private int _paramCount;
private bool _gotConcreteValue;
public WhereBuilderSqlite(Expression filter, bool requireConcreteValue, int seq)
{

View File

@@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation.Extensions;
@@ -61,16 +60,17 @@ namespace NzbDrone.Core.Download
// Get the seed configuration for this release.
// remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie);
// Limit grabs to 2 per second.
if (release.DownloadUrl.IsNotNullOrWhiteSpace() && !release.DownloadUrl.StartsWith("magnet:"))
{
var url = new HttpUri(release.DownloadUrl);
_rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2));
}
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
{
DownloadClient = downloadClient.Name,
DownloadClientId = downloadClient.Definition.Id,
DownloadClientName = downloadClient.Definition.Name,
Redirect = redirect,
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
string downloadClientId;
try
{
@@ -81,19 +81,20 @@ namespace NzbDrone.Core.Download
catch (ReleaseUnavailableException)
{
_logger.Trace("Release {0} no longer available on indexer.", release);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (DownloadClientRejectedReleaseException)
{
_logger.Trace("Release {0} rejected by download client, possible duplicate.", release);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (ReleaseDownloadException ex)
{
var http429 = ex.InnerException as TooManyRequestsException;
if (http429 != null)
if (ex.InnerException is TooManyRequestsException http429)
{
_indexerStatusService.RecordFailure(release.IndexerId, http429.RetryAfter);
}
@@ -102,14 +103,21 @@ namespace NzbDrone.Core.Download
_indexerStatusService.RecordFailure(release.IndexerId);
}
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
_logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, true, source, host, release.Title, release.DownloadUrl, redirect));
if (!string.IsNullOrWhiteSpace(downloadClientId))
{
grabEvent.DownloadId = downloadClientId;
}
_eventAggregator.PublishEvent(grabEvent);
}
public async Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title)
@@ -127,22 +135,35 @@ namespace NzbDrone.Core.Download
var success = false;
var downloadedBytes = Array.Empty<byte>();
var release = new ReleaseInfo
{
Title = title,
DownloadUrl = link,
IndexerId = indexerId,
Indexer = indexer.Definition.Name,
DownloadProtocol = indexer.Protocol
};
var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl)
{
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
try
{
downloadedBytes = await indexer.Download(url);
_indexerStatusService.RecordSuccess(indexerId);
success = true;
grabEvent.Successful = true;
}
catch (ReleaseUnavailableException)
{
_logger.Trace("Release {0} no longer available on indexer.", link);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (ReleaseDownloadException ex)
{
var http429 = ex.InnerException as TooManyRequestsException;
if (http429 != null)
if (ex.InnerException is TooManyRequestsException http429)
{
_indexerStatusService.RecordFailure(indexerId, http429.RetryAfter);
}
@@ -151,17 +172,36 @@ namespace NzbDrone.Core.Download
_indexerStatusService.RecordFailure(indexerId);
}
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_eventAggregator.PublishEvent(grabEvent);
throw;
}
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_logger.Trace("Downloaded {0} bytes from {1}", downloadedBytes.Length, link);
_eventAggregator.PublishEvent(grabEvent);
return downloadedBytes;
}
public void RecordRedirect(string link, int indexerId, string source, string host, string title)
{
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, true, source, host, title, link, true));
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId));
var release = new ReleaseInfo
{
Title = title,
DownloadUrl = link,
IndexerId = indexerId,
Indexer = indexer.Definition.Name,
DownloadProtocol = indexer.Protocol
};
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
{
Redirect = true,
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
_eventAggregator.PublishEvent(grabEvent);
}
}
}

View File

@@ -34,8 +34,8 @@ namespace NzbDrone.Core.HealthCheck
private readonly ICached<HealthCheck> _healthCheckResults;
private bool _hasRunHealthChecksAfterGracePeriod = false;
private bool _isRunningHealthChecksAfterGracePeriod = false;
private bool _hasRunHealthChecksAfterGracePeriod;
private bool _isRunningHealthChecksAfterGracePeriod;
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks,
IServerSideNotificationService serverSideNotificationService,
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.HealthCheck
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
_eventDrivenHealthChecks = GetEventDrivenHealthChecks();
_startupGracePeriodEndTime = runtimeInfo.StartTime + TimeSpan.FromMinutes(15);
_startupGracePeriodEndTime = runtimeInfo.StartTime.AddMinutes(15);
}
public List<HealthCheck> Results()

View File

@@ -52,7 +52,7 @@ namespace NzbDrone.Core.HealthCheck
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
.AddQueryParam("runtime", "netcore")
.AddQueryParam("branch", _configFileProvider.Branch)
.Build();
try

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
List<History> Since(DateTime date, HistoryEventType? eventType);
void Cleanup(int days);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit);
}
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
@@ -115,5 +116,24 @@ namespace NzbDrone.Core.History
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters);
}
}
public History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit)
{
var intEvents = eventTypes.Select(t => (int)t).ToList();
var builder = Builder()
.Where<History>(x => x.IndexerId == indexerId)
.Where<History>(x => x.Date >= date)
.Where<History>(x => intEvents.Contains((int)x.EventType));
var query = Query(builder);
if (limit > 0)
{
query = query.OrderByDescending(h => h.Date).Take(limit).ToList();
}
return query.MinBy(h => h.Date);
}
}
}

View File

@@ -27,6 +27,7 @@ namespace NzbDrone.Core.History
List<History> Between(DateTime start, DateTime end);
List<History> Since(DateTime date, HistoryEventType? eventType);
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit);
}
public class HistoryService : IHistoryService,
@@ -121,7 +122,7 @@ namespace NzbDrone.Core.History
{
Date = DateTime.UtcNow,
IndexerId = message.IndexerId,
EventType = message.Query.RssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
EventType = message.Query.IsRssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
Successful = message.QueryResult.Response?.StatusCode == HttpStatusCode.OK
};
@@ -173,7 +174,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty);
history.Data.Add("Source", message.Query.Source ?? string.Empty);
history.Data.Add("Host", message.Query.Host ?? string.Empty);
history.Data.Add("QueryResults", message.QueryResult.Releases?.Count().ToString() ?? string.Empty);
history.Data.Add("QueryResults", message.QueryResult.Releases?.Count.ToString() ?? string.Empty);
history.Data.Add("Url", message.QueryResult.Response?.Request.Url.FullUri ?? string.Empty);
_historyRepository.Insert(history);
@@ -184,7 +185,7 @@ namespace NzbDrone.Core.History
var history = new History
{
Date = DateTime.UtcNow,
IndexerId = message.IndexerId,
IndexerId = message.Release.IndexerId,
EventType = HistoryEventType.ReleaseGrabbed,
Successful = message.Successful
};
@@ -232,5 +233,10 @@ namespace NzbDrone.Core.History
{
return _historyRepository.CountSince(indexerId, date, eventTypes);
}
public History FindFirstForIndexerSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes, int limit)
{
return _historyRepository.FindFirstForIndexerSince(indexerId, date, eventTypes, limit);
}
}
}

View File

@@ -1,15 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareDetectionService
{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx", "ddos-guard" };
private static readonly HashSet<string> CloudflareServerNames = new () { "cloudflare", "cloudflare-nginx", "ddos-guard" };
private readonly Logger _logger;
public CloudFlareDetectionService(Logger logger)
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Http.CloudFlare
responseHtml.Contains("<title>Access denied</title>") ||
responseHtml.Contains("<title>Attention Required! | Cloudflare</title>") ||
responseHtml.Trim().Equals("error code: 1020") ||
responseHtml.Contains("<title>DDOS-GUARD</title>"))
responseHtml.Contains("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -41,7 +42,7 @@ namespace NzbDrone.Core.Http.CloudFlare
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary == "Accept-Encoding,User-Agent" &&
response.Headers.ContentEncoding == "" &&
response.Headers.ContentEncoding.IsNullOrWhiteSpace() &&
response.Content.ToLower().Contains("ddos"))
{
return true;

View File

@@ -10,17 +10,15 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; }
public string Genre { get; set; }
public override bool RssSearch
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && Author.IsNullOrWhiteSpace() && Title.IsNullOrWhiteSpace())
{
return true;
}
public override bool IsRssSearch =>
SearchTerm.IsNullOrWhiteSpace() &&
!IsIdSearch;
return false;
}
}
public override bool IsIdSearch =>
Author.IsNotNullOrWhiteSpace() ||
Title.IsNotNullOrWhiteSpace() ||
Publisher.IsNotNullOrWhiteSpace() ||
Genre.IsNotNullOrWhiteSpace() ||
Year.HasValue;
}
}

View File

@@ -13,18 +13,17 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; }
public string Genre { get; set; }
public override bool RssSearch
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TmdbId.HasValue && !TraktId.HasValue)
{
return true;
}
public override bool IsRssSearch =>
SearchTerm.IsNullOrWhiteSpace() &&
!IsIdSearch;
return false;
}
}
public override bool IsIdSearch =>
ImdbId.IsNotNullOrWhiteSpace() ||
Genre.IsNotNullOrWhiteSpace() ||
TmdbId.HasValue ||
TraktId.HasValue ||
DoubanId.HasValue ||
Year.HasValue;
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);

View File

@@ -11,17 +11,16 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Track { get; set; }
public int? Year { get; set; }
public override bool RssSearch
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && Album.IsNullOrWhiteSpace() && Artist.IsNullOrWhiteSpace() && Label.IsNullOrWhiteSpace())
{
return true;
}
public override bool IsRssSearch =>
SearchTerm.IsNullOrWhiteSpace() &&
!IsIdSearch;
return false;
}
}
public override bool IsIdSearch =>
Album.IsNotNullOrWhiteSpace() ||
Artist.IsNotNullOrWhiteSpace() ||
Label.IsNotNullOrWhiteSpace() ||
Genre.IsNotNullOrWhiteSpace() ||
Track.IsNotNullOrWhiteSpace() ||
Year.HasValue;
}
}

View File

@@ -7,9 +7,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{
public abstract class SearchCriteriaBase
{
private static readonly Regex SpecialCharacter = new Regex(@"[`'.]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex NonWord = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex StandardizeDashesRegex = new (@"\p{Pd}+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex StandardizeSingleQuotesRegex = new (@"[\u0060\u00B4\u2018\u2019]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public virtual bool InteractiveSearch { get; set; }
public List<int> IndexerIds { get; set; }
@@ -21,58 +20,26 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public string Source { get; set; }
public string Host { get; set; }
public virtual string SearchQuery
public override string ToString() => $"{SearchQuery}, Offset: {Offset ?? 0}, Limit: {Limit ?? 0}, Categories: [{string.Join(", ", Categories)}]";
public virtual string SearchQuery => $"Term: [{SearchTerm}]";
public virtual bool IsRssSearch => SearchTerm.IsNullOrWhiteSpace();
public virtual bool IsIdSearch => false;
public string SanitizedSearchTerm => GetSanitizedTerm(SearchTerm);
private static string GetSanitizedTerm(string term)
{
get
{
return $"Term: [{SearchTerm}]";
}
}
term ??= "";
public override string ToString()
{
return $"{SearchQuery}, Offset: {Offset ?? 0}, Limit: {Limit ?? 0}, Categories: [{string.Join(", ", Categories)}]";
}
term = StandardizeDashesRegex.Replace(term, "-");
term = StandardizeSingleQuotesRegex.Replace(term, "'");
public virtual bool RssSearch
{
get
{
if (SearchTerm.IsNullOrWhiteSpace())
{
return true;
}
var safeTitle = term.Where(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c is '-' or '.' or '_' or '(' or ')' or '@' or '/' or '\'' or '[' or ']' or '+' or '%');
return false;
}
}
public string SanitizedSearchTerm
{
get
{
var term = SearchTerm;
if (SearchTerm == null)
{
term = "";
}
var safeTitle = term.Where(c => (char.IsLetterOrDigit(c)
|| char.IsWhiteSpace(c)
|| c == '-'
|| c == '.'
|| c == '_'
|| c == '('
|| c == ')'
|| c == '@'
|| c == '/'
|| c == '\''
|| c == '['
|| c == ']'
|| c == '+'
|| c == '%'));
return string.Concat(safeTitle);
}
return string.Concat(safeTitle);
}
}
}

View File

@@ -21,23 +21,25 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public int? Year { get; set; }
public string Genre { get; set; }
public string SanitizedTvSearchString => (SanitizedSearchTerm + " " + EpisodeSearchString).Trim();
public string SanitizedTvSearchString => $"{SanitizedSearchTerm} {EpisodeSearchString}".Trim();
public string EpisodeSearchString => GetEpisodeSearchString();
public string FullImdbId => ParseUtil.GetFullImdbId(ImdbId);
public override bool RssSearch
{
get
{
if (SearchTerm.IsNullOrWhiteSpace() && ImdbId.IsNullOrWhiteSpace() && !TvdbId.HasValue && !RId.HasValue && !TraktId.HasValue && !TvMazeId.HasValue)
{
return true;
}
public override bool IsRssSearch =>
SearchTerm.IsNullOrWhiteSpace() &&
!IsIdSearch;
return false;
}
}
public override bool IsIdSearch =>
Episode.IsNotNullOrWhiteSpace() ||
ImdbId.IsNotNullOrWhiteSpace() ||
Season.HasValue ||
TvdbId.HasValue ||
RId.HasValue ||
TraktId.HasValue ||
TvMazeId.HasValue ||
TmdbId.HasValue ||
DoubanId.HasValue;
public override string SearchQuery
{

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Core.IndexerStats
var elapsedTimeEvents = sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
.Select(h => temp);
indexerStats.AverageResponseTime = elapsedTimeEvents.Count() > 0 ? (int)elapsedTimeEvents.Average() : 0;
indexerStats.AverageResponseTime = elapsedTimeEvents.Any() ? (int)elapsedTimeEvents.Average() : 0;
foreach (var historyEvent in sortedEvents)
{

View File

@@ -31,11 +31,12 @@ namespace NzbDrone.Core.IndexerVersions
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 8;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()
// Used when moving yml to C#
private readonly List<string> _definitionBlocklist = new ()
{
"aither",
"animeworld",
"audiobookbay",
"beyond-hd-oneurl",
"beyond-hd",
"blutopia",
@@ -89,7 +90,7 @@ namespace NzbDrone.Core.IndexerVersions
{
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
indexerList = response.Resource.Where(i => !_definitionBlocklist.Contains(i.File)).ToList();
}
catch
{
@@ -125,7 +126,7 @@ namespace NzbDrone.Core.IndexerVersions
public List<string> GetBlocklist()
{
return _defintionBlocklist;
return _definitionBlocklist;
}
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
@@ -227,10 +228,10 @@ namespace NzbDrone.Core.IndexerVersions
if (definition.Settings == null)
{
definition.Settings = new List<SettingsField>
{
new SettingsField { Name = "username", Label = "Username", Type = "text" },
new SettingsField { Name = "password", Label = "Password", Type = "password" }
};
{
new () { Name = "username", Label = "Username", Type = "text" },
new () { Name = "password", Label = "Password", Type = "password" }
};
}
if (definition.Encoding == null)

View File

@@ -9,10 +9,8 @@ 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.Indexers.Settings;
@@ -20,14 +18,13 @@ 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<UserPassTorrentBaseSettings>
{
public override string Name => "Anidub";
public override string[] IndexerUrls => new string[] { "https://tr.anidub.com/" };
public override string[] IndexerUrls => new[] { "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;
@@ -42,31 +39,29 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnidubRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new AnidubRequestGenerator(Settings);
}
public override IParseIndexerResponse GetParser()
{
return new AnidubParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
return new AnidubParser(Settings, Capabilities.Categories, RateLimit, _httpClient, _logger);
}
protected override async Task DoLogin()
{
UpdateCookies(null, null);
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php")
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
var mainPage = await ExecuteAuth(new HttpRequest(Settings.BaseUrl));
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
requestBuilder.SetCookies(mainPage.GetCookies());
var authLoginRequest = requestBuilder
.SetCookies(mainPage.GetCookies())
.AddFormParameter("login_name", Settings.Username)
.AddFormParameter("login_password", Settings.Password)
.AddFormParameter("login", "submit")
@@ -77,27 +72,22 @@ namespace NzbDrone.Core.Indexers.Definitions
if (response.Content != null && !CheckIfLoginNeeded(response))
{
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(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);
var errorMessage = document.QuerySelector("#content .berror .berror_c")?.TextContent.Trim();
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (httpResponse.Content.Contains("index.php?action=logout"))
{
return false;
}
return true;
return !httpResponse.Content.Contains("index.php?action=logout");
}
private IndexerCapabilities SetCapabilities()
@@ -138,31 +128,32 @@ namespace NzbDrone.Core.Indexers.Definitions
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 UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
private readonly UserPassTorrentBaseSettings _settings;
public AnidubRequestGenerator()
public AnidubRequestGenerator(UserPassTorrentBaseSettings settings)
{
_settings = settings;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
{
var requestUrl = string.Empty;
string requestUrl;
var isSearch = !string.IsNullOrWhiteSpace(term);
if (isSearch)
{
requestUrl = string.Format("{0}/index.php?do=search", Settings.BaseUrl.TrimEnd('/'));
requestUrl = $"{_settings.BaseUrl.TrimEnd('/')}/index.php?do=search";
}
else
{
requestUrl = Settings.BaseUrl;
requestUrl = _settings.BaseUrl;
}
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
@@ -207,7 +198,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -216,7 +207,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
return pageableRequests;
}
@@ -225,7 +216,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -234,7 +225,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -243,7 +234,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -256,33 +247,37 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public IIndexerHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
private readonly TimeSpan _rateLimit;
private readonly IIndexerHttpClient _httpClient;
private readonly Logger _logger;
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" }
};
private static Dictionary<string, string> CategoriesMap => new ()
{
{ "/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(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
public AnidubParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
{
_settings = settings;
_categories = categories;
_rateLimit = rateLimit;
_httpClient = httpClient;
_logger = logger;
}
private static string GetTitle(AngleSharp.Html.Dom.IHtmlDocument content, AngleSharp.Dom.IElement tabNode)
@@ -327,9 +322,9 @@ namespace NzbDrone.Core.Indexers.Definitions
private static int GetReleaseLeechers(AngleSharp.Dom.IElement tabNode)
{
const string LeechersSelector = ".list.down > .li_swing_m";
const string leechersSelector = ".list.down > .li_swing_m";
var leechersStr = tabNode.QuerySelector(LeechersSelector).TextContent;
var leechersStr = tabNode.QuerySelector(leechersSelector).TextContent;
int.TryParse(leechersStr, out var leechers);
return leechers;
}
@@ -345,18 +340,18 @@ namespace NzbDrone.Core.Indexers.Definitions
private static int GetReleaseGrabs(AngleSharp.Dom.IElement tabNode)
{
const string GrabsSelector = ".list.down > .li_download_m";
const string grabsSelector = ".list.down > .li_download_m";
var grabsStr = tabNode.QuerySelector(GrabsSelector).TextContent;
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)";
const string dateSelector = ".story_inf > li:nth-child(2)";
var domDate = content.QuerySelector(DateSelector).LastChild;
var domDate = content.QuerySelector(dateSelector).LastChild;
if (domDate?.NodeName != "#text")
{
@@ -397,16 +392,16 @@ namespace NzbDrone.Core.Indexers.Definitions
return utcDate.AddHours(-russianStandardTimeDiff);
}
Logger.Warn($"[AniDub] Date time couldn't be parsed on. Date text: {dateText}");
_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";
const string sizeSelector = ".list.down > .red";
var sizeStr = tabNode.QuerySelector(SizeSelector).TextContent;
var sizeStr = tabNode.QuerySelector(sizeSelector).TextContent;
return ParseUtil.GetBytes(sizeStr);
}
@@ -446,11 +441,11 @@ namespace NzbDrone.Core.Indexers.Definitions
var release = new TorrentInfo
{
Title = GetTitle(dom, t),
InfoUrl = indexerResponse.Request.Url.ToString(),
InfoUrl = indexerResponse.Request.Url.FullUri,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
Guid = indexerResponse.Request.Url.ToString() + t.Id,
Guid = indexerResponse.Request.Url.FullUri + t.Id,
Seeders = GetReleaseSeeders(t),
Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t),
Grabs = GetReleaseGrabs(t),
@@ -472,36 +467,30 @@ namespace NzbDrone.Core.Indexers.Definitions
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);
var links = dom.QuerySelectorAll(".searchitem > h3 > a[href], #dle-content > .story > .story_h > .lcol > h2 > a[href]");
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));
var releaseRequest = new HttpRequestBuilder(url)
.WithRateLimit(_rateLimit.TotalSeconds)
.SetHeader("Referer", _settings.BaseUrl)
.Accept(HttpAccept.Html)
.Build();
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
// Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError)
{
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
}
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
}
torrentInfos.AddRange(ParseRelease(releaseResponse));

View File

@@ -425,7 +425,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
var releaseGroup = releaseTags.LastOrDefault();
if (releaseGroup != null && releaseGroup.Contains("(") && releaseGroup.Contains(")"))
if (releaseGroup != null && releaseGroup.Contains('(') && releaseGroup.Contains(')'))
{
//// Skip raws if set
//if (releaseGroup.ToLowerInvariant().StartsWith("raw") && !AllowRaws)

View File

@@ -7,10 +7,8 @@ using System.Net.Http;
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.Indexers.Settings;
@@ -18,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.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
@@ -26,7 +23,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "AnimeTorrents";
public override string[] IndexerUrls => new string[] { "https://animetorrents.me/" };
public override string[] IndexerUrls => new[] { "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;
@@ -40,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new AnimeTorrentsRequestGenerator { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
@@ -52,30 +49,29 @@ namespace NzbDrone.Core.Indexers.Definitions
{
UpdateCookies(null, null);
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
requestBuilder.SetCookies(loginPage.GetCookies());
var authLoginRequest = requestBuilder
.SetCookies(loginPage.GetCookies())
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("form", "login")
.AddFormParameter("rememberme[]", "1")
.SetHeader("Content-Type", "multipart/form-data")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && response.Content.Contains("logout.php"))
{
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
_logger.Debug("AnimeTorrents authentication succeeded");
}
@@ -87,12 +83,7 @@ namespace NzbDrone.Core.Indexers.Definitions
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php"))
{
return true;
}
return false;
return httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php");
}
private IndexerCapabilities SetCapabilities()
@@ -100,13 +91,13 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie");
@@ -138,10 +129,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public AnimeTorrentsRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var searchString = term;

View File

@@ -5,10 +5,8 @@ using System.Net;
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.Indexers.Settings;
@@ -16,14 +14,13 @@ 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<NoAuthTorrentBaseSettings>
{
public override string Name => "Animedia";
public override string[] IndexerUrls => new string[] { "https://tt.animedia.tv/" };
public override string[] IndexerUrls => new[] { "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;
@@ -38,12 +35,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimediaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new AnimediaRequestGenerator(Settings);
}
public override IParseIndexerResponse GetParser()
{
return new AnimediaParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
return new AnimediaParser(Settings, Capabilities.Categories, RateLimit, _httpClient);
}
private IndexerCapabilities SetCapabilities()
@@ -51,38 +48,40 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
{
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 NoAuthTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
private readonly NoAuthTorrentBaseSettings _settings;
public AnimediaRequestGenerator()
public AnimediaRequestGenerator(NoAuthTorrentBaseSettings settings)
{
_settings = settings;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
{
var requestUrl = string.Empty;
string requestUrl;
if (string.IsNullOrWhiteSpace(term))
{
requestUrl = Settings.BaseUrl;
requestUrl = _settings.BaseUrl;
}
else
{
@@ -94,18 +93,17 @@ namespace NzbDrone.Core.Indexers.Definitions
{ "orderby_sort", "entry_date|desc" }
};
requestUrl = string.Format("{0}/ajax/search_result/P0?{1}", Settings.BaseUrl.TrimEnd('/'), queryCollection.GetQueryString());
requestUrl = $"{_settings.BaseUrl.TrimEnd('/')}/ajax/search_result/P0?{queryCollection.GetQueryString()}";
}
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
yield return request;
yield return new IndexerRequest(requestUrl, HttpAccept.Html);
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -114,7 +112,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}"));
return pageableRequests;
}
@@ -123,7 +121,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
@@ -148,6 +146,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly NoAuthTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly TimeSpan _rateLimit;
private readonly IIndexerHttpClient _httpClient;
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);
@@ -155,25 +156,25 @@ namespace NzbDrone.Core.Indexers.Definitions
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 IIndexerHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
public AnimediaParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories, TimeSpan rateLimit, IIndexerHttpClient httpClient)
{
_settings = settings;
_categories = categories;
_rateLimit = rateLimit;
_httpClient = httpClient;
}
private string composeTitle(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
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 nameRu = dom.QuerySelector("div.media__post__header > h1")?.TextContent.Trim() ?? string.Empty;
var nameEn = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span")?.TextContent.Trim() ?? string.Empty;
var nameOrig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span")?.TextContent.Trim() ?? string.Empty;
var title = name_ru + " / " + name_en;
if (name_en != name_orig)
var title = nameRu + " / " + nameEn;
if (nameEn != nameOrig)
{
title += " / " + name_orig;
title += " / " + nameOrig;
}
var tabName = t.TextContent;
@@ -183,7 +184,7 @@ namespace NzbDrone.Core.Indexers.Definitions
tabName = "";
}
var heading = tr.QuerySelector("h3.tracker_info_bold").TextContent;
var heading = tr.QuerySelector("h3.tracker_info_bold")?.TextContent.Trim() ?? string.Empty;
// Parse episodes info from heading if episods info present
var match = EpisodesInfoQueryRegex.Match(heading);
@@ -192,40 +193,40 @@ namespace NzbDrone.Core.Indexers.Definitions
{
if (string.IsNullOrEmpty(match.Groups[2].Value))
{
heading += " E" + match.Groups[1].Value;
heading += $" E{match.Groups[1].Value}";
}
else
{
heading += string.Format(" E{0}-{1}", match.Groups[1].Value, match.Groups[2].Value);
heading += $" E{match.Groups[1].Value}-{match.Groups[2].Value}";
}
}
return title + " - " + heading + " [" + getResolution(tr) + "p]";
return title + " - " + heading + " [" + GetResolution(tr) + "p]";
}
private string getResolution(AngleSharp.Dom.IElement tr)
private string GetResolution(AngleSharp.Dom.IElement tr)
{
var resolution = tr.QuerySelector("div.tracker_info_left").TextContent;
var resolution = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
return ResolutionInfoQueryRegex.Match(resolution).Groups[1].Value;
}
private long getReleaseSize(AngleSharp.Dom.IElement tr)
private long GetReleaseSize(AngleSharp.Dom.IElement tr)
{
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
var sizeStr = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
return ParseUtil.GetBytes(SizeInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
}
private DateTime getReleaseDate(AngleSharp.Dom.IElement tr)
private DateTime GetReleaseDate(AngleSharp.Dom.IElement tr)
{
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
var sizeStr = tr.QuerySelector("div.tracker_info_left")?.TextContent.Trim() ?? string.Empty;
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;
var rDesc = tr.QuerySelector("h3.tracker_info_bold")?.TextContent.Trim() ?? string.Empty;
var type = dom.QuerySelector("div.releases-date:contains('Тип:')")?.TextContent.Trim() ?? string.Empty;
// Check OVA first cause OVA looks like anime with OVA in release name or description
if (CategorieOVARegex.IsMatch(rName) || CategorieOVARegex.IsMatch(rDesc))
@@ -256,28 +257,28 @@ namespace NzbDrone.Core.Indexers.Definitions
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 trId = t.GetAttribute("href");
var tr = dom.QuerySelector("div" + trId);
var seeders = int.Parse(tr.QuerySelector("div.circle_green_text_top").TextContent);
var url = indexerResponse.HttpRequest.Url.ToString();
var url = indexerResponse.HttpRequest.Url.FullUri;
var release = new TorrentInfo
{
Title = composeTitle(dom, t, tr),
Title = ComposeTitle(dom, t, tr),
InfoUrl = url,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1,
Guid = url + tr_id,
Guid = url + trId,
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)
PublishDate = GetReleaseDate(tr),
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").GetAttribute("href"),
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").GetAttribute("href"),
Size = GetReleaseSize(tr),
Resolution = GetResolution(tr)
};
torrentInfos.Add(release);
}
@@ -291,6 +292,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var links = dom.QuerySelectorAll("a.ads-list__item__title");
foreach (var link in links)
{
@@ -302,20 +304,24 @@ namespace NzbDrone.Core.Indexers.Definitions
url = "https:" + url;
}
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
var releaseRequest = new HttpRequestBuilder(url)
.WithRateLimit(_rateLimit.TotalSeconds)
.SetHeader("Referer", _settings.BaseUrl)
.Accept(HttpAccept.Html)
.Build();
var releaseIndexerRequest = new IndexerRequest(releaseRequest);
var releaseResponse = new IndexerResponse(releaseIndexerRequest, _httpClient.Execute(releaseIndexerRequest.HttpRequest));
// Throw common http errors here before we try to parse
if (releaseResponse.HttpResponse.HasHttpError)
{
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
}
else
{
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
throw new TooManyRequestsException(releaseResponse.HttpRequest, releaseResponse.HttpResponse);
}
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {url}");
}
torrentInfos.AddRange(ParseRelease(releaseResponse));

View File

@@ -52,55 +52,42 @@ namespace NzbDrone.Core.Indexers.Definitions
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
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")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.SetHeader("Referer", LoginUrl)
.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();
var errorMessage = dom.QuerySelector("form#loginform")?.TextContent.Trim();
throw new IndexerAuthException(errorMessage);
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("Anthelion authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
return !httpResponse.Content.Contains("logout.php");
}
private IndexerCapabilities SetCapabilities()
@@ -108,13 +95,13 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
@@ -131,10 +118,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public UserPassTorrentBaseSettings 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('/'));

View File

@@ -29,7 +29,7 @@ public class AroLol : GazelleBase<AroLolSettings>
protected override HttpRequestBuilder AuthLoginRequestBuilder()
{
return base.AuthLoginRequestBuilder()
.AddFormParameter("twofa", Settings.TwoFactorAuthCode.Trim());
.AddFormParameter("twofa", Settings.TwoFactorAuthCode?.Trim() ?? "");
}
protected override bool CheckForLoginError(HttpResponse response)

View File

@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions;
public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
{
public override string Name => "AudioBook Bay";
public override string[] IndexerUrls => new[]
{
"https://audiobookbay.li/",
"https://audiobookbay.se/"
};
public override string[] LegacyUrls => new[]
{
"https://audiobookbay.la/",
"http://audiobookbay.net/",
"https://audiobookbay.unblockit.tv/",
"http://audiobookbay.nl/",
"http://audiobookbay.ws/",
"https://audiobookbay.unblockit.how/",
"https://audiobookbay.unblockit.cam/",
"https://audiobookbay.unblockit.biz/",
"https://audiobookbay.unblockit.day/",
"https://audiobookbay.unblockit.llc/",
"https://audiobookbay.unblockit.blue/",
"https://audiobookbay.unblockit.name/",
"http://audiobookbay.fi/",
"http://audiobookbay.se/",
"http://audiobookbayabb.com/",
"https://audiobookbay.unblockit.ist/",
"https://audiobookbay.unblockit.bet/",
"https://audiobookbay.unblockit.cat/",
"https://audiobookbay.unblockit.nz/",
"https://audiobookbay.fi/",
"https://audiobookbay.unblockit.page/",
"https://audiobookbay.unblockit.pet/",
"https://audiobookbay.unblockit.ink/",
"https://audiobookbay.unblockit.bio/" // error 502
};
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
public override string Language => "en-US";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override int PageSize => 15;
public override IndexerCapabilities Capabilities => SetCapabilities();
public AudioBookBay(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AudioBookBayRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new AudioBookBayParser(Settings, Capabilities.Categories);
}
public override async Task<byte[]> Download(Uri link)
{
var request = new HttpRequestBuilder(link.ToString())
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
.Accept(HttpAccept.Html)
.Build();
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var hash = dom.QuerySelector("td:contains(\"Info Hash:\") ~ td")?.TextContent.Trim();
if (hash == null)
{
throw new Exception($"Failed to fetch hash from {link}");
}
var title = dom.QuerySelector("div.postTitle h1")?.TextContent.Trim();
if (title == null)
{
throw new Exception($"Failed to fetch title from {link}");
}
title = StringUtil.MakeValidFileName(title, '_', false);
var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title);
return await base.Download(new Uri(magnet));
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
};
// Age
caps.Categories.AddCategoryMapping("children", NewznabStandardCategory.AudioAudiobook, "Children");
caps.Categories.AddCategoryMapping("teen-young-adult", NewznabStandardCategory.AudioAudiobook, "Teen & Young Adult");
caps.Categories.AddCategoryMapping("adults", NewznabStandardCategory.AudioAudiobook, "Adults");
// Category
caps.Categories.AddCategoryMapping("postapocalyptic", NewznabStandardCategory.AudioAudiobook, "(Post)apocalyptic");
caps.Categories.AddCategoryMapping("action", NewznabStandardCategory.AudioAudiobook, "Action");
caps.Categories.AddCategoryMapping("adventure", NewznabStandardCategory.AudioAudiobook, "Adventure");
caps.Categories.AddCategoryMapping("art", NewznabStandardCategory.AudioAudiobook, "Art");
caps.Categories.AddCategoryMapping("autobiography-biographies", NewznabStandardCategory.AudioAudiobook, "Autobiography & Biographies");
caps.Categories.AddCategoryMapping("business", NewznabStandardCategory.AudioAudiobook, "Business");
caps.Categories.AddCategoryMapping("computer", NewznabStandardCategory.AudioAudiobook, "Computer");
caps.Categories.AddCategoryMapping("contemporary", NewznabStandardCategory.AudioAudiobook, "Contemporary");
caps.Categories.AddCategoryMapping("crime", NewznabStandardCategory.AudioAudiobook, "Crime");
caps.Categories.AddCategoryMapping("detective", NewznabStandardCategory.AudioAudiobook, "Detective");
caps.Categories.AddCategoryMapping("doctor-who-sci-fi", NewznabStandardCategory.AudioAudiobook, "Doctor Who");
caps.Categories.AddCategoryMapping("education", NewznabStandardCategory.AudioAudiobook, "Education");
caps.Categories.AddCategoryMapping("fantasy", NewznabStandardCategory.AudioAudiobook, "Fantasy");
caps.Categories.AddCategoryMapping("general-fiction", NewznabStandardCategory.AudioAudiobook, "General Fiction");
caps.Categories.AddCategoryMapping("historical-fiction", NewznabStandardCategory.AudioAudiobook, "Historical Fiction");
caps.Categories.AddCategoryMapping("history", NewznabStandardCategory.AudioAudiobook, "History");
caps.Categories.AddCategoryMapping("horror", NewznabStandardCategory.AudioAudiobook, "Horror");
caps.Categories.AddCategoryMapping("humor", NewznabStandardCategory.AudioAudiobook, "Humor");
caps.Categories.AddCategoryMapping("lecture", NewznabStandardCategory.AudioAudiobook, "Lecture");
caps.Categories.AddCategoryMapping("lgbt", NewznabStandardCategory.AudioAudiobook, "LGBT");
caps.Categories.AddCategoryMapping("literature", NewznabStandardCategory.AudioAudiobook, "Literature");
caps.Categories.AddCategoryMapping("litrpg", NewznabStandardCategory.AudioAudiobook, "LitRPG");
caps.Categories.AddCategoryMapping("general-non-fiction", NewznabStandardCategory.AudioAudiobook, "Misc. Non-fiction");
caps.Categories.AddCategoryMapping("mystery", NewznabStandardCategory.AudioAudiobook, "Mystery");
caps.Categories.AddCategoryMapping("paranormal", NewznabStandardCategory.AudioAudiobook, "Paranormal");
caps.Categories.AddCategoryMapping("plays-theater", NewznabStandardCategory.AudioAudiobook, "Plays & Theater");
caps.Categories.AddCategoryMapping("poetry", NewznabStandardCategory.AudioAudiobook, "Poetry");
caps.Categories.AddCategoryMapping("political", NewznabStandardCategory.AudioAudiobook, "Political");
caps.Categories.AddCategoryMapping("radio-productions", NewznabStandardCategory.AudioAudiobook, "Radio Productions");
caps.Categories.AddCategoryMapping("romance", NewznabStandardCategory.AudioAudiobook, "Romance");
caps.Categories.AddCategoryMapping("sci-fi", NewznabStandardCategory.AudioAudiobook, "Sci-Fi");
caps.Categories.AddCategoryMapping("science", NewznabStandardCategory.AudioAudiobook, "Science");
caps.Categories.AddCategoryMapping("self-help", NewznabStandardCategory.AudioAudiobook, "Self-help");
caps.Categories.AddCategoryMapping("spiritual", NewznabStandardCategory.AudioAudiobook, "Spiritual & Religious");
caps.Categories.AddCategoryMapping("sports", NewznabStandardCategory.AudioAudiobook, "Sport & Recreation");
caps.Categories.AddCategoryMapping("suspense", NewznabStandardCategory.AudioAudiobook, "Suspense");
caps.Categories.AddCategoryMapping("thriller", NewznabStandardCategory.AudioAudiobook, "Thriller");
caps.Categories.AddCategoryMapping("true-crime", NewznabStandardCategory.AudioAudiobook, "True Crime");
caps.Categories.AddCategoryMapping("tutorial", NewznabStandardCategory.AudioAudiobook, "Tutorial");
caps.Categories.AddCategoryMapping("westerns", NewznabStandardCategory.AudioAudiobook, "Westerns");
caps.Categories.AddCategoryMapping("zombies", NewznabStandardCategory.AudioAudiobook, "Zombies");
// Category Modifiers
caps.Categories.AddCategoryMapping("anthology", NewznabStandardCategory.AudioAudiobook, "Anthology");
caps.Categories.AddCategoryMapping("bestsellers", NewznabStandardCategory.AudioAudiobook, "Bestsellers");
caps.Categories.AddCategoryMapping("classic", NewznabStandardCategory.AudioAudiobook, "Classic");
caps.Categories.AddCategoryMapping("documentary", NewznabStandardCategory.AudioAudiobook, "Documentary");
caps.Categories.AddCategoryMapping("full-cast", NewznabStandardCategory.AudioAudiobook, "Full Cast");
caps.Categories.AddCategoryMapping("libertarian", NewznabStandardCategory.AudioAudiobook, "Libertarian");
caps.Categories.AddCategoryMapping("military", NewznabStandardCategory.AudioAudiobook, "Military");
caps.Categories.AddCategoryMapping("novel", NewznabStandardCategory.AudioAudiobook, "Novel");
caps.Categories.AddCategoryMapping("short-story", NewznabStandardCategory.AudioAudiobook, "Short Story");
return caps;
}
}
public class AudioBookBayRequestGenerator : IIndexerRequestGenerator
{
private readonly NoAuthTorrentBaseSettings _settings;
private readonly IndexerCapabilities _capabilities;
public AudioBookBayRequestGenerator(NoAuthTorrentBaseSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
{
var searchUrl = _settings.BaseUrl;
var parameters = new NameValueCollection();
term = Regex.Replace(term, @"[\W]+", " ").Trim();
if (term.IsNotNullOrWhiteSpace())
{
parameters.Set("s", term);
parameters.Set("tt", "1");
}
if (parameters.Count > 0)
{
searchUrl += $"?{parameters.GetQueryString()}";
}
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/" }.Uri.AbsoluteUri, HttpAccept.Html);
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/2/" }.Uri.AbsoluteUri, HttpAccept.Html);
yield return new IndexerRequest(new UriBuilder(searchUrl) { Path = "/page/3/" }.Uri.AbsoluteUri, HttpAccept.Html);
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AudioBookBayParser : IParseIndexerResponse
{
private readonly NoAuthTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AudioBookBayParser(NoAuthTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releaseInfos = new List<ReleaseInfo>();
var doc = ParseHtmlDocument(indexerResponse.Content);
var rows = doc.QuerySelectorAll("div.post:has(div[class=\"postTitle\"])");
foreach (var row in rows)
{
var infoUrl = _settings.BaseUrl + row.QuerySelector("div.postTitle h2 a")?.GetAttribute("href")?.Trim().TrimStart('/');
var title = row.QuerySelector("div.postTitle")?.TextContent.Trim();
var infoString = row.QuerySelector("div.postContent")?.TextContent.Trim() ?? string.Empty;
var matchFormat = Regex.Match(infoString, @"Format: (.+) \/", RegexOptions.IgnoreCase);
if (matchFormat.Groups[1].Success && matchFormat.Groups[1].Value.Length > 0 && matchFormat.Groups[1].Value != "?")
{
title += $" [{matchFormat.Groups[1].Value.Trim()}]";
}
var matchBitrate = Regex.Match(infoString, @"Bitrate: (.+)File", RegexOptions.IgnoreCase);
if (matchBitrate.Groups[1].Success && matchBitrate.Groups[1].Value.Length > 0 && matchBitrate.Groups[1].Value != "?")
{
title += $" [{matchBitrate.Groups[1].Value.Trim()}]";
}
var matchSize = Regex.Match(infoString, @"File Size: (.+?)s?$", RegexOptions.IgnoreCase);
var size = matchSize.Groups[1].Success ? ParseUtil.GetBytes(matchSize.Groups[1].Value) : 0;
var matchDateAdded = Regex.Match(infoString, @"Posted: (\d{1,2} \D{3} \d{4})", RegexOptions.IgnoreCase);
var publishDate = matchDateAdded.Groups[1].Success && DateTime.TryParseExact(matchDateAdded.Groups[1].Value, "d MMM yyyy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDate) ? parsedDate : DateTime.Now;
var postInfo = row.QuerySelector("div.postInfo")?.FirstChild?.TextContent.Trim().Replace("\xA0", ";") ?? string.Empty;
var matchCategory = Regex.Match(postInfo, @"Category: (.+)$", RegexOptions.IgnoreCase);
var category = matchCategory.Groups[1].Success ? matchCategory.Groups[1].Value.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() : new List<string>();
var categories = category.SelectMany(_categories.MapTrackerCatDescToNewznab).Distinct().ToList();
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = infoUrl,
Title = CleanTitle(title),
Categories = categories,
Size = size,
Seeders = 1,
Peers = 1,
PublishDate = publishDate,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1
};
var cover = row.QuerySelector("img[src]")?.GetAttribute("src")?.Trim();
if (!string.IsNullOrEmpty(cover))
{
release.PosterUrl = cover.StartsWith("http") ? cover : _settings.BaseUrl + cover;
}
releaseInfos.Add(release);
}
return releaseInfos;
}
private static IHtmlDocument ParseHtmlDocument(string response)
{
var parser = new HtmlParser();
var doc = parser.ParseDocument(response);
var hidden = doc.QuerySelectorAll("div.post.re-ab");
foreach (var element in hidden)
{
var body = doc.CreateElement("div");
body.ClassList.Add("post");
body.InnerHtml = Encoding.UTF8.GetString(Convert.FromBase64String(element.TextContent));
element.Parent.ReplaceChild(body, element);
}
return doc;
}
private static string CleanTitle(string title)
{
title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled);
title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
return title.Trim();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

View File

@@ -66,12 +66,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
protected override bool CheckIfLoginNeeded(HttpResponse response)
{
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed)
{
return true;
}
return false;
return response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed;
}
protected override void ModifyRequest(IndexerRequest request)
@@ -99,14 +94,14 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
}
return null;
@@ -116,12 +111,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true
LogResponseContent = true,
Method = HttpMethod.Post
};
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)

View File

@@ -7,10 +7,8 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom;
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.Indexers.Settings;
@@ -18,14 +16,13 @@ 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 BB : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "BB";
public override string[] IndexerUrls => new string[] { StringUtil.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") };
public override string[] IndexerUrls => new[] { StringUtil.FromBase64("aHR0cHM6Ly9iYWNvbmJpdHMub3JnLw==") };
private string LoginUrl => Settings.BaseUrl + "login.php";
public override string Description => "BB is a Private Torrent Tracker for 0DAY / GENERAL";
public override string Language => "en-US";
@@ -41,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new BBRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new BBRequestGenerator { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
@@ -54,30 +51,22 @@ namespace NzbDrone.Core.Indexers.Definitions
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
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")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.SetHeader("Referer", LoginUrl)
.Build();
var headers = new NameValueCollection
{
{ "Referer", LoginUrl }
};
authLoginRequest.Headers.Add(headers);
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
@@ -98,19 +87,14 @@ namespace NzbDrone.Core.Indexers.Definitions
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("BB authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
return !httpResponse.Content.Contains("logout.php");
}
private IndexerCapabilities SetCapabilities()
@@ -118,21 +102,21 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
{
MovieSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
@@ -163,10 +147,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public BBRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));

View File

@@ -8,7 +8,6 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
@@ -19,7 +18,6 @@ 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
{
@@ -27,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "BakaBT";
public override string[] IndexerUrls => new string[] { "https://bakabt.me/" };
public override string[] IndexerUrls => new[] { "https://bakabt.me/" };
public override string Description => "Anime Community";
private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
@@ -41,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new BakaBTRequestGenerator { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
@@ -52,14 +50,14 @@ namespace NzbDrone.Core.Indexers.Definitions
public override async Task<byte[]> Download(Uri link)
{
var request = new HttpRequestBuilder(link.ToString())
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
.Build();
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
.Build();
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var downloadLink = dom.QuerySelectorAll(".download_link").First().GetAttribute("href");
var downloadLink = dom.QuerySelector(".download_link")?.GetAttribute("href");
if (string.IsNullOrWhiteSpace(downloadLink))
{
@@ -76,19 +74,12 @@ namespace NzbDrone.Core.Indexers.Definitions
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
requestBuilder.SetCookies(loginPage.GetCookies());
requestBuilder.AddFormParameter("username", Settings.Username);
requestBuilder.AddFormParameter("password", Settings.Password);
requestBuilder.AddFormParameter("returnto", "/index.php");
var parser = new HtmlParser();
var dom = parser.ParseDocument(loginPage.Content);
var loginKey = dom.QuerySelector("input[name=\"loginKey\"]");
@@ -98,14 +89,18 @@ namespace NzbDrone.Core.Indexers.Definitions
}
var authLoginRequest = requestBuilder
.SetHeader("Content-Type", "multipart/form-data")
.SetCookies(loginPage.GetCookies())
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("returnto", "/index.php")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && response.Content.Contains("<a href=\"logout.php\">Logout</a>"))
{
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
_logger.Debug("BakaBT authentication succeeded");
}
@@ -117,12 +112,7 @@ namespace NzbDrone.Core.Indexers.Definitions
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>"))
{
return true;
}
return false;
return !httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>");
}
private IndexerCapabilities SetCapabilities()
@@ -130,21 +120,21 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
{
MovieSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "Anime Series");
@@ -166,10 +156,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public BakaBTSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public BakaBTRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
{
var searchString = term;
@@ -245,7 +231,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
private readonly BakaBTSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly List<IndexerCategory> _defaultCategories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
private readonly List<IndexerCategory> _defaultCategories = new () { NewznabStandardCategory.TVAnime };
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories)
{
@@ -295,7 +281,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var stringSeparator = new[] { " | " };
var titles = titleSeries.Split(stringSeparator, StringSplitOptions.RemoveEmptyEntries);
if (titles.Count() > 1 && !_settings.AddRomajiTitle)
if (titles.Length > 1 && !_settings.AddRomajiTitle)
{
titles = titles.Skip(1).ToArray();
}
@@ -307,7 +293,7 @@ namespace NzbDrone.Core.Indexers.Definitions
release.Title = (name + releaseInfo).Trim();
// Ensure the season is defined as this tracker only deals with full seasons
if (release.Title.IndexOf("Season") == -1 && _settings.AppendSeason)
if (!release.Title.Contains("Season", StringComparison.CurrentCulture) && _settings.AppendSeason)
{
// Insert before the release info
var aidx = release.Title.IndexOf('(');
@@ -415,10 +401,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public class BakaBTSettings : UserPassTorrentBaseSettings
{
public BakaBTSettings()
{
}
[FieldDefinition(4, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
public bool AddRomajiTitle { get; set; }

View File

@@ -1,17 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -53,7 +52,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
var generator = _generatorCache.Get(Settings.DefinitionFile, () =>
new CardigannRequestGenerator(_configService,
_definitionService.GetCachedDefinition(Settings.DefinitionFile),
_logger)
_logger,
RateLimit)
{
HttpClient = _httpClient,
Definition = Definition,
@@ -79,6 +79,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
};
}
protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases, SearchCriteriaBase searchCriteria)
{
var cleanReleases = base.CleanupReleases(releases, searchCriteria);
if (_definitionService.GetCachedDefinition(Settings.DefinitionFile).Search?.Rows?.Filters?.Any(x => x.Name == "andmatch") ?? false)
{
cleanReleases = FilterReleasesByQuery(releases, searchCriteria).ToList();
}
return cleanReleases;
}
protected override IDictionary<string, string> GetCookies()
{
if (Settings.ExtraFieldData.TryGetValue("cookie", out var cookies))
@@ -180,60 +192,15 @@ namespace NzbDrone.Core.Indexers.Cardigann
await generator.DoLogin();
}
public override async Task<byte[]> Download(Uri link)
protected override async Task<HttpRequest> GetDownloadRequest(Uri link)
{
var generator = (CardigannRequestGenerator)GetRequestGenerator();
var request = await generator.DownloadRequest(link);
if (request.Url.Scheme == "magnet")
{
ValidateMagnet(request.Url.FullUri);
return Encoding.UTF8.GetBytes(request.Url.FullUri);
}
request.AllowAutoRedirect = true;
var downloadBytes = Array.Empty<byte>();
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", request.Url.FullUri);
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
}
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.Error("API Grab Limit reached for {0}", request.Url.FullUri);
}
else
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", request.Url.FullUri);
}
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (WebException ex)
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", request.Url.FullUri);
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Downloading torrent failed");
throw;
}
return downloadBytes;
return request;
}
protected override async Task Test(List<ValidationFailure> failures)

View File

@@ -300,7 +300,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
else if (setting.Type == "checkbox")
{
variables[name] = ((bool)value) ? ".True" : null;
if (value is string stringValue && bool.TryParse(stringValue, out var result))
{
value = result;
}
variables[name] = (bool)value ? ".True" : null;
}
else if (setting.Type == "select")
{
@@ -328,12 +333,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
else
{
throw new NotSupportedException();
throw new NotSupportedException($"Type {setting.Type} is not supported.");
}
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging)
if (setting.Type != "password" && setting.Name != "apikey" && setting.Name != "rsskey" && indexerLogging && variables.ContainsKey(name))
{
_logger.Debug($"Setting {setting.Name} to {variables[name]}");
_logger.Debug($"Setting {setting.Name} to {variables[name].ToJson()}");
}
}
@@ -758,8 +763,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
return data;
}
protected Dictionary<string, string> ParseCustomHeaders(Dictionary<string, List<string>> customHeaders,
Dictionary<string, object> variables)
protected Dictionary<string, string> ParseCustomHeaders(Dictionary<string, List<string>> customHeaders, Dictionary<string, object> variables)
{
if (customHeaders == null)
{

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public double? RequestDelay { get; set; }
public List<string> Links { get; set; }
public List<string> Legacylinks { get; set; }
public bool Followredirect { get; set; } = false;
public bool Followredirect { get; set; }
public bool TestLinkTorrent { get; set; } = true;
public List<string> Certificates { get; set; }
public CapabilitiesBlock Caps { get; set; }
@@ -95,13 +95,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
public List<string> Cookies { get; set; }
public string Method { get; set; }
public string Form { get; set; }
public bool Selectors { get; set; } = false;
public bool Selectors { get; set; }
public Dictionary<string, string> Inputs { get; set; }
public Dictionary<string, SelectorBlock> Selectorinputs { get; set; }
public Dictionary<string, SelectorBlock> Getselectorinputs { get; set; }
public List<ErrorBlock> Error { get; set; }
public PageTestBlock Test { get; set; }
public CaptchaBlock Captcha { get; set; }
public Dictionary<string, List<string>> Headers { get; set; }
}
public class ErrorBlock
@@ -114,7 +115,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public class SelectorBlock
{
public string Selector { get; set; }
public bool Optional { get; set; } = false;
public bool Optional { get; set; }
public string Text { get; set; }
public string Attribute { get; set; }
public string Remove { get; set; }
@@ -157,7 +158,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
public int After { get; set; }
public SelectorBlock Dateheaders { get; set; }
public SelectorBlock Count { get; set; }
public bool Multiple { get; set; } = false;
public bool Multiple { get; set; }
}
public class SearchPathBlock : RequestBlock
@@ -182,20 +183,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
public string Method { get; set; }
public BeforeBlock Before { get; set; }
public InfohashBlock Infohash { get; set; }
public Dictionary<string, List<string>> Headers { get; set; }
}
public class InfohashBlock
{
public SelectorField Hash { get; set; }
public SelectorField Title { get; set; }
public bool UseBeforeResponse { get; set; }
public bool Usebeforeresponse { get; set; }
}
public class SelectorField
{
public string Selector { get; set; }
public string Attribute { get; set; }
public bool UseBeforeResponse { get; set; }
public bool Usebeforeresponse { get; set; }
public List<FilterBlock> Filters { get; set; }
}

View File

@@ -40,12 +40,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
// Remove cookie cache
if (indexerResponse.HttpResponse.HasHttpRedirect && indexerResponse.HttpResponse.RedirectUrl
.ContainsIgnoreCase("login.php"))
if (indexerResponse.HttpResponse.HasHttpRedirect)
{
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("login.php"))
{
// Remove cookie cache
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from API request");
}
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
@@ -62,7 +66,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
if (request.SearchPath.Response != null &&
request.SearchPath.Response.NoResultsMessage != null &&
((request.SearchPath.Response.NoResultsMessage != string.Empty && results.Contains(request.SearchPath.Response.NoResultsMessage)) || (request.SearchPath.Response.NoResultsMessage == string.Empty && results == string.Empty)))
((request.SearchPath.Response.NoResultsMessage.IsNotNullOrWhiteSpace() && results.Contains(request.SearchPath.Response.NoResultsMessage)) || (request.SearchPath.Response.NoResultsMessage.IsNullOrWhiteSpace() && results.IsNullOrWhiteSpace())))
{
return releases;
}

View File

@@ -4,7 +4,6 @@ using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
@@ -29,11 +28,15 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected IHtmlDocument landingResultDocument;
protected override string SiteLink => ResolveSiteLink();
private readonly TimeSpan _rateLimit;
public CardigannRequestGenerator(IConfigService configService,
CardigannDefinition definition,
Logger logger)
Logger logger,
TimeSpan rateLimit)
: base(configService, definition, logger)
{
_rateLimit = rateLimit;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
@@ -190,6 +193,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var login = _definition.Login;
var variables = GetBaseTemplateVariables();
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
if (login.Method == "post")
{
var pairs = new Dictionary<string, string>();
@@ -218,15 +224,26 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
requestBuilder.Headers.Add("Referer", SiteLink);
Cookies = null;
if (login.Cookies != null)
{
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
}
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
var request = requestBuilder
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", SiteLink)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
}
else if (login.Method == "form")
{
@@ -235,13 +252,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
var queryCollection = new NameValueCollection();
var pairs = new Dictionary<string, string>();
var formSelector = login.Form;
if (formSelector == null)
{
formSelector = "form";
}
var formSelector = login.Form ?? "form";
// landingResultDocument might not be initiated if the login is caused by a relogin during a query
// landingResultDocument might not be initiated if the login is caused by a re-login during a query
if (landingResultDocument == null)
{
await GetConfigurationForSetup(true);
@@ -273,11 +286,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
continue;
}
var value = input.GetAttribute("value");
if (value == null)
{
value = "";
}
var value = input.GetAttribute("value") ?? "";
pairs[name] = value;
}
@@ -356,11 +365,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.SetCookies(Cookies);
var request = requestBuilder
.SetCookies(Cookies)
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", loginUrl)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
var simpleCaptchaResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
var simpleCaptchaJSON = JObject.Parse(simpleCaptchaResult.Content);
var captchaSelection = simpleCaptchaJSON["images"][0]["hash"].ToString();
@@ -398,7 +410,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
var enctype = form.GetAttribute("enctype");
if (enctype == "multipart/form-data")
{
var headers = new Dictionary<string, string>();
var boundary = "---------------------------" + DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString().Replace(".", "");
var bodyParts = new List<string>();
@@ -424,21 +435,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.Headers.Add("Referer", SiteLink);
requestBuilder.SetCookies(Cookies);
foreach (var pair in pairs)
{
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
foreach (var header in headers)
{
requestBuilder.SetHeader(header.Key, header.Value);
}
var request = requestBuilder
.SetCookies(Cookies)
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", SiteLink)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
var request = requestBuilder.Build();
request.SetContent(body);
loginResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
@@ -454,26 +462,30 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.SetCookies(Cookies);
requestBuilder.Headers.Add("Referer", loginUrl);
foreach (var pair in pairs)
{
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
loginResult = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
var request = requestBuilder
.SetCookies(Cookies)
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", loginUrl)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
loginResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
}
Cookies = loginResult.GetCookies();
CheckForError(loginResult, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
}
else if (login.Method == "cookie")
{
CookiesUpdater(null, null);
Settings.ExtraFieldData.TryGetValue("cookie", out var cookies);
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30));
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now.AddDays(30));
}
else if (login.Method == "get")
{
@@ -496,15 +508,19 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.Headers.Add("Referer", SiteLink);
var request = requestBuilder
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", SiteLink)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
}
else if (login.Method == "oneurl")
{
@@ -521,19 +537,23 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.Headers.Add("Referer", SiteLink);
var request = requestBuilder
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", SiteLink)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
var response = await HttpClient.ExecuteProxiedAsync(requestBuilder.Build(), Definition);
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
CookiesUpdater(Cookies, DateTime.Now.AddDays(30));
}
else
{
throw new NotImplementedException("Login method " + login.Method + " not implemented");
throw new NotImplementedException($"Login method {login.Method} not implemented");
}
}
@@ -578,15 +598,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
return null;
}
var variables = GetBaseTemplateVariables();
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
var loginUrl = ResolvePath(login.Path);
Cookies = null;
if (login.Cookies != null)
{
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
}
var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri)
{
LogResponseContent = true,
@@ -594,14 +610,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
Encoding = _encoding
};
requestBuilder.Headers.Add("Referer", SiteLink);
if (Cookies != null)
Cookies = null;
if (login.Cookies != null)
{
requestBuilder.SetCookies(Cookies);
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
}
var request = requestBuilder.Build();
var request = requestBuilder
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", SiteLink)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
landingResult = await HttpClient.ExecuteProxiedAsync(request, Definition);
@@ -634,6 +654,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var captcha = login.Captcha;
var variables = GetBaseTemplateVariables();
var headers = ParseCustomHeaders(_definition.Login?.Headers ?? _definition.Search?.Headers, variables);
if (captcha.Type == "image")
{
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
@@ -644,8 +667,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = new HttpRequestBuilder(captchaUrl.ToString())
.SetCookies(landingResult.GetCookies())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", loginUrl.AbsoluteUri)
.SetEncoding(_encoding)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
var response = await HttpClient.ExecuteProxiedAsync(request, Definition);
@@ -656,10 +681,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
ImageData = response.ResponseData
};
}
else
{
_logger.Debug("CardigannIndexer ({0}): No captcha image found", _definition.Id);
}
_logger.Debug("CardigannIndexer ({0}): No captcha image found", _definition.Id);
}
else
{
@@ -689,8 +712,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var requestLinkStr = ResolvePath(ApplyGoTemplateText(request.Path, variables)).ToString();
_logger.Debug("CardigannIndexer ({0}): handleRequest() requestLinkStr= {1}", _definition.Id, requestLinkStr);
Dictionary<string, string> pairs = null;
var queryCollection = new NameValueCollection();
@@ -724,25 +745,36 @@ namespace NzbDrone.Core.Indexers.Cardigann
requestLinkStr += queryCollection.GetQueryString(_encoding, separator: request.Queryseparator);
}
var httpRequest = new HttpRequestBuilder(requestLinkStr)
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetEncoding(_encoding)
.SetHeader("Referer", referer);
httpRequest.Method = method;
var httpRequestBuilder = new HttpRequestBuilder(requestLinkStr)
{
Method = method,
Encoding = _encoding
};
// Add form data for POST requests
if (method == HttpMethod.Post)
{
foreach (var param in pairs)
{
httpRequest.AddFormParameter(param.Key, param.Value);
httpRequestBuilder.AddFormParameter(param.Key, param.Value);
}
}
var response = await HttpClient.ExecuteProxiedAsync(httpRequest.Build(), Definition);
var headers = ParseCustomHeaders(_definition.Download?.Headers ?? _definition.Search?.Headers, variables);
var httpRequest = httpRequestBuilder
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetHeader("Referer", referer)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
_logger.Debug("CardigannIndexer ({0}): handleRequest() httpRequest={1}", _definition.Id, httpRequest);
var response = await HttpClient.ExecuteProxiedAsync(httpRequest, Definition);
_logger.Debug("CardigannIndexer ({0}): handleRequest() remote server returned {1}", _definition.Id, response.StatusCode);
return response;
}
@@ -750,11 +782,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
Cookies = GetCookies();
var method = HttpMethod.Get;
var headers = new Dictionary<string, string>();
var variables = GetBaseTemplateVariables();
AddTemplateVariablesFromUri(variables, link, ".DownloadUri");
headers = ParseCustomHeaders(_definition.Search?.Headers, variables);
var headers = ParseCustomHeaders(_definition.Download?.Headers ?? _definition.Search?.Headers, variables);
if (_definition.Download != null)
{
@@ -766,6 +797,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetEncoding(_encoding)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
request.AllowAutoRedirect = true;
@@ -791,7 +823,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
try
{
if (!download.Infohash.UseBeforeResponse || download.Before == null || response == null)
if (!download.Infohash.Usebeforeresponse || download.Before == null || response == null)
{
response = await HttpClient.ExecuteProxiedAsync(request, Definition);
}
@@ -799,13 +831,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
var hash = MatchSelector(response, download.Infohash.Hash, variables);
if (hash == null)
{
throw new CardigannException($"InfoHash selectors didn't match");
throw new CardigannException("InfoHash selectors didn't match hash.");
}
var title = MatchSelector(response, download.Infohash.Title, variables);
if (title == null)
{
throw new CardigannException($"InfoHash selectors didn't match");
throw new CardigannException("InfoHash selectors didn't match title.");
}
var magnet = MagnetLinkBuilder.BuildPublicMagnetLink(hash, title);
@@ -837,7 +869,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
try
{
if (!selector.UseBeforeResponse || download.Before == null || response == null)
if (!selector.Usebeforeresponse || download.Before == null || response == null)
{
response = await HttpClient.ExecuteProxiedAsync(request, Definition);
}
@@ -856,6 +888,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetEncoding(_encoding)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
response = await HttpClient.ExecuteProxiedAsync(testLinkRequest, Definition);
@@ -875,6 +908,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetEncoding(_encoding)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
selectorDownloadRequest.Method = method;
@@ -895,6 +929,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.SetCookies(Cookies ?? new Dictionary<string, string>())
.SetHeaders(headers ?? new Dictionary<string, string>())
.SetEncoding(_encoding)
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
downloadRequest.Method = method;
@@ -907,8 +942,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
var selectorText = ApplyGoTemplateText(selector.Selector, variables);
var parser = new HtmlParser();
var results = response.Content;
var resultDocument = parser.ParseDocument(results);
var resultDocument = parser.ParseDocument(response.Content);
var element = resultDocument.QuerySelector(selectorText);
if (element == null)
@@ -1096,16 +1130,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
_logger.Info($"Adding request: {searchUrl}");
var requestbuilder = new HttpRequestBuilder(searchUrl);
requestbuilder.Method = method;
var requestBuilder = new HttpRequestBuilder(searchUrl)
{
Method = method,
Encoding = _encoding
};
// Add FormData for searchs that POST
if (method == HttpMethod.Post)
{
foreach (var param in queryCollection)
{
requestbuilder.AddFormParameter(param.Key, param.Value);
requestBuilder.AddFormParameter(param.Key, param.Value);
}
}
@@ -1113,14 +1149,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
if (search.Headers != null)
{
var headers = ParseCustomHeaders(search.Headers, variables);
requestbuilder.SetHeaders(headers ?? new Dictionary<string, string>());
requestBuilder.SetHeaders(headers ?? new Dictionary<string, string>());
}
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
var request = requestBuilder
.WithRateLimit(_rateLimit.TotalSeconds)
.Build();
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
var cardigannRequest = new CardigannRequest(request, variables, searchPath)
{
HttpRequest =
{
AllowAutoRedirect = searchPath.Followredirect
}
};
yield return request;
yield return cardigannRequest;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -18,54 +19,58 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetDefaultParameters();
var parameters = new NameValueCollection();
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() || searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Set("action", "search-torrents");
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
parameters.Add("type", "imdb");
parameters.Add("query", searchCriteria.FullImdbId);
parameters.Set("type", "imdb");
parameters.Set("query", searchCriteria.FullImdbId);
}
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("type", "name");
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
if (searchCriteria.Season.HasValue)
{
parameters.Add("season", searchCriteria.Season.ToString());
parameters.Add("episode", searchCriteria.Episode);
parameters.Set("season", searchCriteria.Season.ToString());
}
if (searchCriteria.Episode.IsNotNullOrWhiteSpace())
{
parameters.Set("episode", searchCriteria.Episode);
}
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetDefaultParameters();
var parameters = new NameValueCollection();
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Add("type", "imdb");
parameters.Add("query", searchCriteria.FullImdbId);
parameters.Set("action", "search-torrents");
parameters.Set("type", "imdb");
parameters.Set("query", searchCriteria.FullImdbId);
}
else if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Add("type", "name");
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
parameters.Set("action", "search-torrents");
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
@@ -73,16 +78,16 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetDefaultParameters();
var parameters = new NameValueCollection();
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Add("type", "name");
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
parameters.Set("action", "search-torrents");
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
@@ -90,16 +95,16 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetDefaultParameters();
var parameters = new NameValueCollection();
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Add("type", "name");
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
parameters.Set("action", "search-torrents");
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
@@ -107,47 +112,47 @@ public class FileListRequestGenerator : IIndexerRequestGenerator
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetDefaultParameters();
var parameters = new NameValueCollection();
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace())
{
parameters.Add("action", "search-torrents");
parameters.Add("type", "name");
parameters.Add("query", searchCriteria.SanitizedSearchTerm.Trim());
parameters.Set("action", "search-torrents");
parameters.Set("type", "name");
parameters.Set("query", searchCriteria.SanitizedSearchTerm.Trim());
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
{
if (parameters.Get("action") is null)
{
parameters.Add("action", "latest-torrents");
parameters.Set("action", "latest-torrents");
}
parameters.Add("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api.php?{parameters.GetQueryString()}";
yield return new IndexerRequest(searchUrl, HttpAccept.Json);
}
private NameValueCollection GetDefaultParameters()
{
var parameters = new NameValueCollection
if (searchCriteria.Categories != null && searchCriteria.Categories.Any())
{
{ "username", Settings.Username.Trim() },
{ "passkey", Settings.Passkey.Trim() }
};
parameters.Set("category", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories)));
}
if (Settings.FreeleechOnly)
{
parameters.Add("freeleech", "1");
parameters.Set("freeleech", "1");
}
return parameters;
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/api.php?{parameters.GetQueryString()}";
var request = new IndexerRequest(searchUrl, HttpAccept.Json)
{
HttpRequest =
{
Credentials = new BasicNetworkCredential(Settings.Username.Trim(), Settings.Passkey.Trim())
}
};
yield return request;
}
}

View File

@@ -0,0 +1,343 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions;
public class FunFile : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "FunFile";
public override string[] IndexerUrls => new[] { "https://www.funfile.org/" };
public override string Description => "FunFile is a general tracker";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1");
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public FunFile(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new FunFileRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new FunFileParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "takelogin.php")
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("returnto", "")
.AddFormParameter("login", "Login")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
{
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("td.mf_content")?.TextContent.Trim();
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
var cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("Authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return !httpResponse.Content.Contains("logout.php");
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(44, NewznabStandardCategory.TVAnime, "Anime");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.PC, "Applications");
caps.Categories.AddCategoryMapping(43, NewznabStandardCategory.AudioAudiobook, "Audio Books");
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.Books, "Ebook");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Games");
caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.OtherMisc, "Miscellaneous");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Movies, "Movies");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Music");
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.PCMobileOther, "Portable");
caps.Categories.AddCategoryMapping(49, NewznabStandardCategory.Other, "Tutorials");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TV, "TV");
return caps;
}
}
public class FunFileRequestGenerator : IIndexerRequestGenerator
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilities _capabilities;
public FunFileRequestGenerator(UserPassTorrentBaseSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories, searchCriteria.FullImdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{
var parameters = new NameValueCollection
{
{ "cat", "0" },
{ "incldead", "1" },
{ "showspam", "1" },
{ "s_title", "1" }
};
if (imdbId.IsNotNullOrWhiteSpace())
{
parameters.Set("search", imdbId);
parameters.Set("s_desc", "1");
}
else if (term.IsNotNullOrWhiteSpace())
{
parameters.Set("search", term);
}
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Any())
{
queryCats.ForEach(cat => parameters.Set($"c{cat}", "1"));
}
var searchUrl = _settings.BaseUrl + "browse.php";
if (parameters.Count > 0)
{
searchUrl += $"?{parameters.GetQueryString()}";
}
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class FunFileParser : IParseIndexerResponse
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly List<string> _validTagList = new ()
{
"action",
"adventure",
"animation",
"biography",
"comedy",
"crime",
"documentary",
"drama",
"family",
"fantasy",
"game-show",
"history",
"home_&_garden",
"home_and_garden",
"horror",
"music",
"musical",
"mystery",
"news",
"reality",
"reality-tv",
"romance",
"sci-fi",
"science-fiction",
"short",
"sport",
"talk-show",
"thriller",
"travel",
"war",
"western"
};
private readonly char[] _delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' };
public FunFileParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releaseInfos = new List<TorrentInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("table.mainframe table[cellpadding=\"2\"] > tbody > tr:has(td.row3)");
foreach (var row in rows)
{
var qDownloadLink = row.QuerySelector("a[href^=\"download.php\"]");
if (qDownloadLink == null)
{
throw new Exception("Download links not found. Make sure you can download from the website.");
}
var downloadUrl = _settings.BaseUrl + qDownloadLink.GetAttribute("href");
var qDetailsLink = row.QuerySelector("a[href^=\"details.php?id=\"]");
var title = qDetailsLink?.GetAttribute("title")?.Trim();
var infoUrl = _settings.BaseUrl + qDetailsLink?.GetAttribute("href")?.Replace("&hit=1", "");
var categoryLink = row.QuerySelector("a[href^=\"browse.php?cat=\"]")?.GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat");
var seeders = ParseUtil.CoerceInt(row.Children[9].TextContent);
var leechers = ParseUtil.CoerceInt(row.Children[10].TextContent);
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadUrl,
Title = title,
Categories = _categories.MapTrackerCatToNewznab(cat),
Size = ParseUtil.GetBytes(row.Children[7].TextContent),
Files = ParseUtil.CoerceInt(row.Children[3].TextContent),
Grabs = ParseUtil.CoerceInt(row.Children[8].TextContent),
Seeders = seeders,
Peers = leechers + seeders,
PublishDate = DateTimeUtil.FromTimeAgo(row.Children[5].TextContent),
DownloadVolumeFactor = 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 172800 // 48 hours
};
var nextRow = row.NextElementSibling;
if (nextRow != null)
{
var qStats = nextRow.QuerySelector("table > tbody > tr:nth-child(3)");
release.UploadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[0].TextContent.Replace("X", ""));
release.DownloadVolumeFactor = ParseUtil.CoerceDouble(qStats?.Children[1].TextContent.Replace("X", ""));
release.Description = nextRow.QuerySelector("span[style=\"float:left\"]")?.TextContent.Trim();
var genres = release.Description.ToLower().Replace(" & ", "_&_").Replace(" and ", "_and_");
var releaseGenres = _validTagList.Intersect(genres.Split(_delimiters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
release.Genres = releaseGenres.Select(x => x.Replace("_", " ")).ToList();
}
releaseInfos.Add(release);
}
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

View File

@@ -56,7 +56,7 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
CheckForLoginError(response);
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("Gazelle authentication succeeded.");
}
@@ -68,7 +68,6 @@ public abstract class GazelleBase<TSettings> : TorrentIndexerBase<TSettings>
LogResponseContent = true,
Method = HttpMethod.Post
};
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var authLoginRequestBuilder = requestBuilder
.AddFormParameter("username", Settings.Username)

View File

@@ -46,7 +46,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
if (searchCriteria.ImdbId != null)
{
@@ -62,9 +62,9 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && searchCriteria.Artist != "VA")
{
parameters.Set("artistname", searchCriteria.Artist);
}
@@ -104,7 +104,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
pageableRequests.Add(GetRequest(parameters));
return pageableRequests;
@@ -114,7 +114,7 @@ public class GazelleRequestGenerator : IIndexerRequestGenerator
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
pageableRequests.Add(GetRequest(parameters));
return pageableRequests;

View File

@@ -104,6 +104,7 @@ public class GreatPosterWallRequestGenerator : GazelleRequestGenerator
public class GreatPosterWallParser : GazelleParser
{
private readonly GreatPosterWallSettings _settings;
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
public GreatPosterWallParser(GreatPosterWallSettings settings, IndexerCapabilities capabilities)
: base(settings, capabilities)
@@ -113,21 +114,27 @@ public class GreatPosterWallParser : GazelleParser
public override IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var releaseInfos = new List<ReleaseInfo>();
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
// Remove cookie cache
CookiesUpdater(null, null);
if (indexerResponse.HttpResponse.HasHttpRedirect)
{
if (indexerResponse.HttpResponse.RedirectUrl.ContainsIgnoreCase("login.php"))
{
// Remove cookie cache
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Redirected to {indexerResponse.HttpResponse.RedirectUrl} from API request");
}
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
// Remove cookie cache
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
@@ -136,7 +143,7 @@ public class GreatPosterWallParser : GazelleParser
jsonResponse.Resource.Status.IsNullOrWhiteSpace() ||
jsonResponse.Resource.Response == null)
{
return torrentInfos;
return releaseInfos;
}
foreach (var result in jsonResponse.Resource.Response.Results)
@@ -148,15 +155,13 @@ public class GreatPosterWallParser : GazelleParser
var release = new GazelleInfo
{
MinimumRatio = 1,
MinimumSeedTime = 172800,
Title = torrent.FileName,
InfoUrl = infoUrl,
Title = WebUtility.HtmlDecode(torrent.FileName).Trim(),
Guid = infoUrl,
InfoUrl = infoUrl,
PosterUrl = GetPosterUrl(result.Cover),
DownloadUrl = GetDownloadUrl(torrent.TorrentId, torrent.CanUseToken),
PublishDate = new DateTimeOffset(time, TimeSpan.FromHours(8)).UtcDateTime, // Time is Chinese Time, add 8 hours difference from UTC
Categories = new List<IndexerCategory> { NewznabStandardCategory.Movies },
Categories = ParseCategories(torrent),
Size = torrent.Size,
Seeders = torrent.Seeders,
Peers = torrent.Seeders + torrent.Leechers,
@@ -164,11 +169,12 @@ public class GreatPosterWallParser : GazelleParser
Files = torrent.FileCount,
Scene = torrent.Scene,
DownloadVolumeFactor = torrent.IsFreeleech || torrent.IsNeutralLeech || torrent.IsPersonalFreeleech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1,
MinimumRatio = 1,
MinimumSeedTime = 172800 // 48 hours
};
var imdbId = ParseUtil.GetImdbID(result.ImdbId);
if (imdbId != null)
{
release.ImdbId = (int)imdbId;
@@ -194,11 +200,11 @@ public class GreatPosterWallParser : GazelleParser
break;
}
torrentInfos.Add(release);
releaseInfos.Add(release);
}
}
return torrentInfos
return releaseInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
@@ -213,6 +219,22 @@ public class GreatPosterWallParser : GazelleParser
return url.FullUri;
}
private List<IndexerCategory> ParseCategories(GreatPosterWallTorrent torrent)
{
var cats = new List<IndexerCategory>
{
NewznabStandardCategory.Movies,
torrent.Resolution switch
{
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD,
"2160p" => NewznabStandardCategory.MoviesUHD,
_ => NewznabStandardCategory.MoviesSD
}
};
return cats;
}
}
public class GreatPosterWallSettings : GazelleSettings
@@ -223,180 +245,76 @@ public class GreatPosterWallSettings : GazelleSettings
public class GreatPosterWallResponse
{
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("response")]
public Response Response { get; set; }
public GreatPosterWallResponseWithResults Response { get; set; }
}
public class Response
public class GreatPosterWallResponseWithResults
{
[JsonProperty("currentPage")]
public int CurrentPage { get; set; }
public int CurrentPage { get; set; }
public int Pages { get; set; }
[JsonProperty("pages")]
public int Pages { get; set; }
[JsonProperty("results")]
public List<Result> Results { get; set; }
[JsonProperty("results")]
public List<GreatPosterWallResult> Results { get; set; }
}
public class Result
public class GreatPosterWallResult
{
[JsonProperty("groupId")]
public int GroupId { get; set; }
[JsonProperty("groupName")]
public string GroupName { get; set; }
[JsonProperty("groupSubName")]
public string GroupSubName { get; set; }
[JsonProperty("cover")]
public string Cover { get; set; }
[JsonProperty("tags")]
public List<string> Tags { get; set; }
[JsonProperty("bookmarked")]
public bool Bookmarked { get; set; }
[JsonProperty("groupYear")]
public int GroupYear { get; set; }
[JsonProperty("releaseType")]
public string ReleaseType { get; set; }
[JsonProperty("groupTime")]
public string GroupTime { get; set; }
[JsonProperty("maxSize")]
public object MaxSize { get; set; }
[JsonProperty("totalSnatched")]
public int TotalSnatched { get; set; }
[JsonProperty("totalSeeders")]
public int TotalSeeders { get; set; }
[JsonProperty("totalLeechers")]
public int TotalLeechers { get; set; }
[JsonProperty("imdbId")]
public string ImdbId { get; set; }
[JsonProperty("imdbRating")]
public string ImdbRating { get; set; }
[JsonProperty("imdbVote")]
public string ImdbVote { get; set; }
[JsonProperty("doubanId")]
public string DoubanId { get; set; }
[JsonProperty("doubanRating")]
public string DoubanRating { get; set; }
[JsonProperty("doubanVote")]
public string DoubanVote { get; set; }
[JsonProperty("rtRating")]
public string RtRating { get; set; }
[JsonProperty("region")]
public string Region { get; set; }
[JsonProperty("torrents")]
public List<GreatPosterWallTorrent> Torrents { get; set; }
public int GroupId { get; set; }
public string GroupName { get; set; }
public string GroupSubName { get; set; }
public string Cover { get; set; }
public List<string> Tags { get; set; }
public bool Bookmarked { get; set; }
public int GroupYear { get; set; }
public string ReleaseType { get; set; }
public string GroupTime { get; set; }
public object MaxSize { get; set; }
public int TotalSnatched { get; set; }
public int TotalSeeders { get; set; }
public int TotalLeechers { get; set; }
public string ImdbId { get; set; }
public string ImdbRating { get; set; }
public string ImdbVote { get; set; }
public string DoubanId { get; set; }
public string DoubanRating { get; set; }
public string DoubanVote { get; set; }
public string RtRating { get; set; }
public string Region { get; set; }
[JsonProperty("torrents")]
public List<GreatPosterWallTorrent> Torrents { get; set; }
}
public class GreatPosterWallTorrent
{
[JsonProperty("torrentId")]
public int TorrentId { get; set; }
[JsonProperty("editionId")]
public int EditionId { get; set; }
[JsonProperty("remasterYear")]
public int RemasterYear { get; set; }
[JsonProperty("remasterTitle")]
public string RemasterTitle { get; set; }
[JsonProperty("remasterCustomTitle")]
public string RemasterCustomTitle { get; set; }
[JsonProperty("scene")]
public bool Scene { get; set; }
[JsonProperty("jinzhuan")]
public bool Jinzhuan { get; set; }
[JsonProperty("fileCount")]
public int FileCount { get; set; }
[JsonProperty("time")]
public DateTime Time { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("snatches")]
public int Snatches { get; set; }
[JsonProperty("seeders")]
public int Seeders { get; set; }
[JsonProperty("leechers")]
public int Leechers { get; set; }
[JsonProperty("isFreeleech")]
public bool IsFreeleech { get; set; }
[JsonProperty("isNeutralLeech")]
public bool IsNeutralLeech { get; set; }
[JsonProperty("freeType")]
public string FreeType { get; set; }
[JsonProperty("isPersonalFreeleech")]
public bool IsPersonalFreeleech { get; set; }
[JsonProperty("canUseToken")]
public bool CanUseToken { get; set; }
[JsonProperty("hasSnatched")]
public bool HasSnatched { get; set; }
[JsonProperty("resolution")]
public string Resolution { get; set; }
[JsonProperty("source")]
public string Source { get; set; }
[JsonProperty("codec")]
public string Codec { get; set; }
[JsonProperty("container")]
public string Container { get; set; }
[JsonProperty("processing")]
public string Processing { get; set; }
[JsonProperty("chineseDubbed")]
public string ChineseDubbed { get; set; }
[JsonProperty("specialSub")]
public string SpecialSub { get; set; }
[JsonProperty("subtitles")]
public string Subtitles { get; set; }
[JsonProperty("fileName")]
public string FileName { get; set; }
[JsonProperty("releaseGroup")]
public string ReleaseGroup { get; set; }
public int TorrentId { get; set; }
public int EditionId { get; set; }
public int RemasterYear { get; set; }
public string RemasterTitle { get; set; }
public string RemasterCustomTitle { get; set; }
public bool Scene { get; set; }
public bool Jinzhuan { get; set; }
public int FileCount { get; set; }
public DateTime Time { get; set; }
public long Size { get; set; }
public int Snatches { get; set; }
public int Seeders { get; set; }
public int Leechers { get; set; }
public bool IsFreeleech { get; set; }
public bool IsNeutralLeech { get; set; }
public string FreeType { get; set; }
public bool IsPersonalFreeleech { get; set; }
public bool CanUseToken { get; set; }
public bool HasSnatched { get; set; }
public string Resolution { get; set; }
public string Source { get; set; }
public string Codec { get; set; }
public string Container { get; set; }
public string Processing { get; set; }
public string ChineseDubbed { get; set; }
public string SpecialSub { get; set; }
public string Subtitles { get; set; }
public string FileName { get; set; }
public string ReleaseGroup { get; set; }
}

View File

@@ -1,21 +1,20 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class HDBits : TorrentIndexerBase<HDBitsSettings>
{
public override string Name => "HDBits";
public override string[] IndexerUrls => new string[] { "https://hdbits.org" };
public override string[] IndexerUrls => new[] { "https://hdbits.org/" };
public override string[] LegacyUrls => new[] { "https://hdbits.org" };
public override string Description => "Best HD Tracker";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public override int PageSize => 30;
public HDBits(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
@@ -25,7 +24,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new HDBitsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new HDBitsRequestGenerator { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
@@ -38,13 +37,13 @@ namespace NzbDrone.Core.Indexers.HDBits
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
}
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
}
};
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Audio, "Audio Track");

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class TorrentQuery
{

View File

@@ -1,6 +1,6 @@
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class HDBitsInfo : TorrentInfo
{

View File

@@ -7,7 +7,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class HDBitsParser : IParseIndexerResponse
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using NzbDrone.Common.Extensions;
@@ -8,7 +9,7 @@ using NzbDrone.Common.Serializer;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class HDBitsRequestGenerator : IIndexerRequestGenerator
{
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Indexers.HDBits
if (imdbId != 0)
{
query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo();
query.ImdbInfo ??= new ImdbInfo();
query.ImdbInfo.Id = imdbId;
}
@@ -91,15 +92,23 @@ namespace NzbDrone.Core.Indexers.HDBits
if (tvdbId != 0)
{
query.TvdbInfo = query.TvdbInfo ?? new TvdbInfo();
query.TvdbInfo ??= new TvdbInfo();
query.TvdbInfo.Id = tvdbId;
query.TvdbInfo.Season = searchCriteria.Season;
query.TvdbInfo.Episode = searchCriteria.Episode;
if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
{
query.Search = showDate.ToString("yyyy-MM-dd");
}
else
{
query.TvdbInfo.Season = searchCriteria.Season;
query.TvdbInfo.Episode = searchCriteria.Episode;
}
}
if (imdbId != 0)
{
query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo();
query.ImdbInfo ??= new ImdbInfo();
query.ImdbInfo.Id = imdbId;
}

View File

@@ -4,7 +4,7 @@ using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.HDBits
namespace NzbDrone.Core.Indexers.Definitions.HDBits
{
public class HDBitsSettingsValidator : NoAuthSettingsValidator<HDBitsSettings>
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
@@ -58,10 +59,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Method = HttpMethod.Post
};
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
@@ -86,19 +84,14 @@ namespace NzbDrone.Core.Indexers.Definitions
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("HDSpace authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
return !httpResponse.Content.Contains("logout.php");
}
private IndexerCapabilities SetCapabilities()
@@ -155,26 +148,30 @@ namespace NzbDrone.Core.Indexers.Definitions
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdb = null)
{
var searchUrl = string.Format("{0}/index.php?page=torrents&", Settings.BaseUrl.TrimEnd('/'));
var queryCollection = new NameValueCollection
{
{ "page", "torrents" },
{ "active", "0" },
{ "category", string.Join(";", Capabilities.Categories.MapTorznabCapsToTrackers(categories)) }
};
if (imdb != null)
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (catList.Any())
{
queryCollection.Add("options", "2");
queryCollection.Add("search", imdb);
queryCollection.Set("category", string.Join(";", catList));
}
if (imdb.IsNotNullOrWhiteSpace())
{
queryCollection.Set("options", "2");
queryCollection.Set("search", imdb);
}
else
{
queryCollection.Add("options", "0");
queryCollection.Add("search", term.Replace(".", " "));
queryCollection.Set("options", "0");
queryCollection.Set("search", term.Replace(".", " "));
}
searchUrl += queryCollection.GetQueryString();
var searchUrl = $"{Settings.BaseUrl.TrimEnd('/')}/index.php?{queryCollection.GetQueryString()}";
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
@@ -267,15 +264,10 @@ namespace NzbDrone.Core.Indexers.Definitions
release.DownloadUrl = _settings.BaseUrl + downloadUrl;
// Use the torrent filename as release title
var torrentTitle = ParseUtil.GetArgumentFromQueryString(downloadUrl, "f")?
.Replace("&amp;", "&")
.Replace("&#039;", "'")
.Replace(".torrent", "")
.Trim();
var torrentTitle = ParseUtil.GetArgumentFromQueryString(downloadUrl, "f")?.Replace(".torrent", "").Trim();
if (torrentTitle.IsNotNullOrWhiteSpace())
{
release.Title = torrentTitle;
release.Title = WebUtility.HtmlDecode(torrentTitle);
}
var qGenres = row.QuerySelector("td:nth-child(2) span[style=\"color: #000000 \"]");
@@ -318,8 +310,9 @@ namespace NzbDrone.Core.Indexers.Definitions
}
release.UploadVolumeFactor = 1;
var qCat = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]");
var cat = qCat.GetAttribute("href").Split('=')[2];
var categoryLink = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
release.Categories = _categories.MapTrackerCatToNewznab(cat);
torrentInfos.Add(release);

View File

@@ -58,11 +58,9 @@ namespace NzbDrone.Core.Indexers.Definitions
Method = HttpMethod.Post
};
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("uid", Settings.Username)
.AddFormParameter("pwd", Settings.Password)
@@ -73,7 +71,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var response = await ExecuteAuth(authLoginRequest);
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("HDTorrents authentication succeeded.");
}
@@ -222,7 +220,7 @@ namespace NzbDrone.Core.Indexers.Definitions
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
private readonly Regex _posterRegex = new Regex(@"src=\\'./([^']+)\\'", RegexOptions.IgnoreCase);
private readonly Regex _posterRegex = new (@"src=\\'./([^']+)\\'", RegexOptions.IgnoreCase);
private readonly HashSet<string> _freeleechRanks = new (StringComparer.OrdinalIgnoreCase)
{
"VIP",
@@ -263,15 +261,14 @@ namespace NzbDrone.Core.Indexers.Definitions
: null;
var link = new Uri(_settings.BaseUrl + row.Children[4].FirstElementChild.GetAttribute("href"));
var description = row.Children[2].QuerySelector("span").TextContent;
var description = row.Children[2].QuerySelector("span")?.TextContent.Trim();
var size = ParseUtil.GetBytes(row.Children[7].TextContent);
var dateTag = row.Children[6].FirstElementChild;
var dateString = string.Join(" ", dateTag.Attributes.Select(attr => attr.Name));
var publishDate = DateTime.ParseExact(dateString, "dd MMM yyyy HH:mm:ss zz00", CultureInfo.InvariantCulture).ToLocalTime();
var dateAdded = string.Join(" ", row.Children[6].FirstElementChild.Attributes.Select(a => a.Name).Take(4));
var publishDate = DateTime.ParseExact(dateAdded, "dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
var catStr = row.FirstElementChild.FirstElementChild.GetAttribute("href").Split('=')[1];
var cat = _categories.MapTrackerCatToNewznab(catStr);
var categoryLink = row.FirstElementChild.FirstElementChild.GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
// Sometimes the uploader column is missing, so seeders, leechers, and grabs may be at a different index.
// There's room for improvement, but this works for now.
@@ -340,12 +337,13 @@ namespace NzbDrone.Core.Indexers.Definitions
var release = new TorrentInfo
{
Title = title,
Description = description,
Guid = details.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
InfoUrl = details.AbsoluteUri,
PosterUrl = poster,
PublishDate = publishDate,
Categories = cat,
Categories = _categories.MapTrackerCatToNewznab(cat),
ImdbId = imdb ?? 0,
Size = size,
Grabs = grabs,

View File

@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5);
private string LoginUrl => Settings.BaseUrl + "takelogin.php";
public ImmortalSeed(IIndexerHttpClient httpClient,
@@ -57,7 +58,6 @@ namespace NzbDrone.Core.Indexers.Definitions
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
var cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("ImmortalSeed authentication succeeded.");
}
@@ -213,7 +213,13 @@ namespace NzbDrone.Core.Indexers.Definitions
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var parameters = new NameValueCollection();
var parameters = new NameValueCollection
{
{ "category", "0" },
{ "include_dead_torrents", "yes" },
{ "sort", "added" },
{ "order", "desc" }
};
term = Regex.Replace(term, @"[ -._]+", " ").Trim();
@@ -222,12 +228,9 @@ namespace NzbDrone.Core.Indexers.Definitions
parameters.Set("do", "search");
parameters.Set("keywords", term);
parameters.Set("search_type", "t_name");
parameters.Set("category", "0");
parameters.Set("include_dead_torrents", "no");
}
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Any())
{
parameters.Set("selectedcats2", string.Join(",", queryCats));
@@ -309,7 +312,7 @@ namespace NzbDrone.Core.Indexers.Definitions
release.PublishDate = DateTime.ParseExact(dateAddedMatch.Value, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture);
}
if (row.QuerySelector("img[title^=\"Free Torrent\"]") != null)
if (row.QuerySelector("img[title^=\"Free Torrent\"], img[title^=\"Sitewide Free Torrent\"]") != null)
{
release.DownloadVolumeFactor = 0;
}

View File

@@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
@@ -21,352 +21,348 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
namespace NzbDrone.Core.Indexers.Definitions;
public class Libble : TorrentIndexerBase<LibbleSettings>
{
internal class Libble : TorrentIndexerBase<LibbleSettings>
public override string Name => "Libble";
public override string[] IndexerUrls => new[] { "https://libble.me/" };
public override string Description => "Libble is a Private Torrent Tracker for MUSIC";
private string LoginUrl => Settings.BaseUrl + "login.php";
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 int PageSize => 50;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Libble(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
public override string Name => "Libble";
public override string[] IndexerUrls => new string[] { "https://libble.me/" };
public override string Description => "Libble is a Private Torrent Tracker for MUSIC";
private string LoginUrl => Settings.BaseUrl + "login.php";
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 int PageSize => 50;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Libble(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new LibbleRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new LibbleParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
Method = HttpMethod.Post,
AllowAutoRedirect = true
};
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("code", Settings.TwoFactorAuthCode)
.AddFormParameter("keeplogged", "1")
.AddFormParameter("login", "Login")
.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("#loginform > .warning")?.TextContent.Trim();
throw new IndexerAuthException($"Libble authentication failed. Error: \"{errorMessage}\"");
}
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Libble authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return !httpResponse.Content.Contains("logout.php");
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Label, MusicSearchParam.Year, MusicSearchParam.Genre
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Audio);
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.AudioVideo);
return caps;
}
}
public class LibbleRequestGenerator : IIndexerRequestGenerator
public override IIndexerRequestGenerator GetRequestGenerator()
{
public LibbleSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public LibbleRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
{
var term = searchCriteria.SanitizedSearchTerm.Trim();
parameters.Add("order_by", "time");
parameters.Add("order_way", "desc");
parameters.Add("searchstr", term);
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Count > 0)
{
foreach (var cat in queryCats)
{
parameters.Add($"filter_cat[{cat}]", "1");
}
}
if (searchCriteria.Offset.HasValue && searchCriteria.Limit.HasValue && searchCriteria.Offset > 0 && searchCriteria.Limit > 0)
{
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
parameters.Add("page", page.ToString());
}
var searchUrl = string.Format("{0}/torrents.php?{1}", Settings.BaseUrl.TrimEnd('/'), parameters.GetQueryString());
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
{
parameters.Add("artistname", searchCriteria.Artist);
}
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
{
parameters.Add("groupname", searchCriteria.Album);
}
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
{
parameters.Add("recordlabel", searchCriteria.Label);
}
if (searchCriteria.Year.HasValue)
{
parameters.Add("year", searchCriteria.Year.ToString());
}
if (searchCriteria.Genre.IsNotNullOrWhiteSpace())
{
parameters.Add("taglist", searchCriteria.Genre);
}
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
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; }
return new LibbleRequestGenerator(Settings, Capabilities);
}
public class LibbleParser : IParseIndexerResponse
public override IParseIndexerResponse GetParser()
{
private readonly LibbleSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
return new LibbleParser(Settings);
}
public LibbleParser(LibbleSettings settings, IndexerCapabilitiesCategories categories)
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
_settings = settings;
_categories = categories;
}
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("code", Settings.TwoFactorAuthCode)
.AddFormParameter("keeplogged", "1")
.AddFormParameter("login", "Login")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.SetHeader("Referer", LoginUrl)
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var doc = parser.ParseDocument(indexerResponse.Content);
var rows = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
var releaseYearRegex = new Regex(@"\[(\d{4})\]$");
foreach (var row in rows)
{
var albumLinkNode = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]");
var groupId = ParseUtil.GetArgumentFromQueryString(albumLinkNode.GetAttribute("href"), "id");
var artistsNodes = row.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]");
var releaseArtist = "Various Artists";
if (artistsNodes.Count() > 0)
{
releaseArtist = artistsNodes.Select(artist => artist.TextContent.Trim()).ToList().Join(", ");
}
var releaseAlbumName = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]")?.TextContent.Trim();
var title = row.QuerySelector("td:nth-child(4) > strong")?.TextContent.Trim();
var releaseAlbumYear = releaseYearRegex.Match(title);
var releaseDescription = row.QuerySelector("div.tags")?.TextContent.Trim();
var releaseThumbnailUrl = row.QuerySelector(".thumbnail")?.GetAttribute("title").Trim();
var releaseGenres = new List<string>();
if (!string.IsNullOrEmpty(releaseDescription))
{
releaseGenres = releaseGenres.Union(releaseDescription.Split(',').Select(tag => tag.Trim()).ToList()).ToList();
}
var cat = row.QuerySelector("td.cats_col div.cat_icon")?.GetAttribute("class").Trim();
var matchCategory = Regex.Match(cat, @"\bcats_(.*?)\b");
if (matchCategory.Success)
{
cat = matchCategory.Groups[1].Value.Trim();
}
var category = new List<IndexerCategory>
{
cat switch
{
"music" => NewznabStandardCategory.Audio,
"libblemixtapes" => NewznabStandardCategory.Audio,
"musicvideos" => NewznabStandardCategory.AudioVideo,
_ => NewznabStandardCategory.Other,
}
};
var releaseRows = doc.QuerySelectorAll(string.Format("table#torrent_table > tbody > tr.group_torrent.groupid_{0}:has(a[href*=\"torrents.php?id=\"])", groupId));
foreach (var releaseRow in releaseRows)
{
var release = new TorrentInfo();
var detailsNode = releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"]");
var downloadLink = _settings.BaseUrl + releaseRow.QuerySelector("a[href^=\"torrents.php?action=download&id=\"]").GetAttribute("href").Trim();
var releaseTags = detailsNode.FirstChild.TextContent.Trim(' ', '/');
release.Title = string.Format("{0} - {1} {2} {3}", releaseArtist, releaseAlbumName, releaseAlbumYear, releaseTags).Trim();
release.Categories = category;
release.Description = releaseDescription;
release.Genres = releaseGenres;
release.PosterUrl = releaseThumbnailUrl;
release.InfoUrl = _settings.BaseUrl + detailsNode.GetAttribute("href").Trim();
release.DownloadUrl = downloadLink;
release.Guid = release.InfoUrl;
release.Size = ParseUtil.GetBytes(releaseRow.QuerySelector("td:nth-child(4)").TextContent.Trim());
release.Files = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(2)").TextContent);
release.Grabs = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(5)").TextContent);
release.Seeders = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(6)").TextContent);
release.Peers = release.Seeders + ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(7)").TextContent);
release.MinimumRatio = 1;
release.MinimumSeedTime = 259200; // 72 hours
try
{
release.PublishDate = DateTime.ParseExact(
releaseRow.QuerySelector("td:nth-child(3) > span[title]").GetAttribute("title").Trim(),
"MMM dd yyyy, HH:mm",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal);
}
catch (Exception)
{
}
switch (releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"] strong")?.TextContent.Trim())
{
case "Neutral!":
release.DownloadVolumeFactor = 0;
release.UploadVolumeFactor = 0;
break;
case "Freeleech!":
release.DownloadVolumeFactor = 0;
release.UploadVolumeFactor = 1;
break;
default:
release.DownloadVolumeFactor = 1;
release.UploadVolumeFactor = 1;
break;
}
torrentInfos.Add(release);
}
}
return torrentInfos.ToArray();
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now.AddDays(30));
_logger.Debug("Authentication succeeded.");
}
public class LibbleSettings : UserPassTorrentBaseSettings
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
public LibbleSettings()
{
TwoFactorAuthCode = "";
}
return !httpResponse.Content.Contains("logout.php");
}
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the Libble Web Site. Otherwise just leave it empty.")]
public string TwoFactorAuthCode { get; set; }
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Label, MusicSearchParam.Year, MusicSearchParam.Genre
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio, "Music");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Audio, "Libble Mixtapes");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.AudioVideo, "Music Videos");
return caps;
}
}
public class LibbleRequestGenerator : IIndexerRequestGenerator
{
private readonly LibbleSettings _settings;
private readonly IndexerCapabilities _capabilities;
public LibbleRequestGenerator(LibbleSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && searchCriteria.Artist != "VA")
{
parameters.Set("artistname", searchCriteria.Artist);
}
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
{
// Remove year
var album = Regex.Replace(searchCriteria.Album, @"(.+)\b\d{4}$", "$1");
parameters.Set("groupname", album.Trim());
}
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
{
parameters.Set("recordlabel", searchCriteria.Label);
}
if (searchCriteria.Year.HasValue)
{
parameters.Set("year", searchCriteria.Year.ToString());
}
if (searchCriteria.Genre.IsNotNullOrWhiteSpace())
{
parameters.Set("taglist", searchCriteria.Genre);
parameters.Set("tags_type", "0");
}
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
{
var term = searchCriteria.SanitizedSearchTerm.Trim();
parameters.Set("action", "advanced");
parameters.Set("order_by", "time");
parameters.Set("order_way", "desc");
if (term.IsNotNullOrWhiteSpace())
{
parameters.Set("searchstr", term);
}
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Any())
{
queryCats.ForEach(cat => parameters.Set($"filter_cat[{cat}]", "1"));
}
if (searchCriteria.Offset.HasValue && searchCriteria.Limit.HasValue && searchCriteria.Offset > 0 && searchCriteria.Limit > 0)
{
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
parameters.Set("page", page.ToString());
}
var searchUrl = $"{_settings.BaseUrl.TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}";
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class LibbleParser : IParseIndexerResponse
{
private readonly LibbleSettings _settings;
private static Regex ReleaseYearRegex => new (@"\[(\d{4})\]$", RegexOptions.Compiled);
public LibbleParser(LibbleSettings settings)
{
_settings = settings;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var doc = parser.ParseDocument(indexerResponse.Content);
var groups = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
foreach (var group in groups)
{
var albumLinkNode = group.QuerySelector("strong > a[href*=\"torrents.php?id=\"]");
var groupId = ParseUtil.GetArgumentFromQueryString(albumLinkNode.GetAttribute("href"), "id");
var artistsNodes = group.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]");
var releaseArtist = "Various Artists";
if (artistsNodes.Any())
{
releaseArtist = artistsNodes.Select(artist => artist.TextContent.Trim()).ToList().Join(", ");
}
var releaseAlbumName = group.QuerySelector("strong > a[href*=\"torrents.php?id=\"]")?.TextContent.Trim();
var title = group.QuerySelector("td:nth-child(4) > strong")?.TextContent.Trim();
var releaseAlbumYear = ReleaseYearRegex.Match(title);
var releaseDescription = group.QuerySelector("div.tags")?.TextContent.Trim();
var releaseThumbnailUrl = group.QuerySelector(".thumbnail")?.GetAttribute("title")?.Trim();
var releaseGenres = new List<string>();
if (!string.IsNullOrEmpty(releaseDescription))
{
releaseGenres = releaseDescription.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
}
var rows = doc.QuerySelectorAll($"table#torrent_table > tbody > tr.group_torrent.groupid_{groupId}:has(a[href*=\"torrents.php?id=\"])");
foreach (var row in rows)
{
var detailsNode = row.QuerySelector("a[href^=\"torrents.php?id=\"]");
var infoUrl = _settings.BaseUrl + detailsNode.GetAttribute("href").Trim();
var downloadLink = _settings.BaseUrl + row.QuerySelector("a[href^=\"torrents.php?action=download&id=\"]").GetAttribute("href").Trim();
var releaseTags = detailsNode.FirstChild?.TextContent.Trim(' ', '/');
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadLink,
Title = $"{releaseArtist} - {releaseAlbumName} {releaseAlbumYear.Value} {releaseTags}".Trim(' ', '-'),
Artist = releaseArtist,
Album = releaseAlbumName,
Categories = ParseCategories(group),
Description = releaseDescription,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(4)").TextContent.Trim()),
Files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(2)").TextContent),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(5)").TextContent),
Seeders = seeders,
Peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent),
DownloadVolumeFactor = 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 259200, // 72 hours,
Genres = releaseGenres,
PosterUrl = releaseThumbnailUrl,
};
try
{
var dateAdded = row.QuerySelector("td:nth-child(3) > span[title]").GetAttribute("title").Trim();
release.PublishDate = DateTime.ParseExact(dateAdded, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
}
catch (Exception)
{
release.PublishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(3)")?.TextContent.Trim());
}
switch (row.QuerySelector("a[href^=\"torrents.php?id=\"] strong")?.TextContent.ToLower().Trim(' ', '!'))
{
case "neutral":
release.DownloadVolumeFactor = 0;
release.UploadVolumeFactor = 0;
break;
case "freeleech":
release.DownloadVolumeFactor = 0;
release.UploadVolumeFactor = 1;
break;
}
releaseInfos.Add(release);
}
}
return releaseInfos.ToArray();
}
private IList<IndexerCategory> ParseCategories(IElement group)
{
var cat = group.QuerySelector("td.cats_col div.cat_icon")?.GetAttribute("class")?.Trim();
var matchCategory = Regex.Match(cat, @"\bcats_(.*?)\b");
if (matchCategory.Success)
{
cat = matchCategory.Groups[1].Value.Trim();
}
return new List<IndexerCategory>
{
cat switch
{
"music" => NewznabStandardCategory.Audio,
"libblemixtapes" => NewznabStandardCategory.Audio,
"musicvideos" => NewznabStandardCategory.AudioVideo,
_ => NewznabStandardCategory.Other,
}
};
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class LibbleSettings : UserPassTorrentBaseSettings
{
public LibbleSettings()
{
TwoFactorAuthCode = "";
}
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the Libble Web Site. Otherwise just leave it empty.")]
public string TwoFactorAuthCode { get; set; }
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
@@ -27,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override bool SupportsRedirect => true;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Nebulance(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
@@ -44,6 +46,19 @@ namespace NzbDrone.Core.Indexers.Definitions
return new NebulanceParser(Settings);
}
protected override Task<HttpRequest> GetDownloadRequest(Uri link)
{
// Avoid using cookies to prevent redirects to login page
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri)
{
AllowAutoRedirect = FollowRedirect
};
var request = requestBuilder.Build();
return Task.FromResult(request);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities

View File

@@ -211,7 +211,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
}
}
}

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